From f86a6790a230f956bcaebd184aa0b323a1f14e91 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Tue, 28 Apr 2026 19:48:33 -0400 Subject: [PATCH] refactor(llm): move queryParams off model.native to typed field Promotes queryParams to a first-class ModelRef field used by Endpoint.baseURL, so deployment-level URL query params (Azure api-version, OpenAI-compatible provider knobs) live in a typed home instead of an opaque `native` bag. Also removes write-only dead fields from `native`: - openaiCompatibleProvider (set by family helper, never read) - opencodeProviderID, opencodeModelID (set by opencode bridge + native session builder, never read) - npm (set by opencode bridge, never read) After this commit `model.native` only carries genuinely provider-specific opaque options that no other adapter cares about (Bedrock's aws_credentials + aws_region for SigV4). Drops the now-dead ProviderShared.queryParams helper. Updates AGENTS.md doc on native is implicit through the new schema JSDoc. --- packages/llm/src/endpoint.ts | 9 ++++----- packages/llm/src/provider/openai-compatible-chat.ts | 10 +++------- packages/llm/src/provider/shared.ts | 9 --------- packages/llm/src/schema.ts | 13 +++++++++++++ packages/llm/test/provider/openai-chat.test.ts | 2 +- .../test/provider/openai-compatible-chat.test.ts | 5 +---- packages/llm/test/provider/openai-responses.test.ts | 2 +- packages/opencode/src/provider/llm-bridge.ts | 7 +------ packages/opencode/src/session/llm-native.ts | 6 +----- packages/opencode/test/provider/llm-bridge.test.ts | 4 ++-- packages/opencode/test/session/llm-native.test.ts | 4 ++-- 11 files changed, 29 insertions(+), 42 deletions(-) diff --git a/packages/llm/src/endpoint.ts b/packages/llm/src/endpoint.ts index 31a3a2e6e5..bd85bcf604 100644 --- a/packages/llm/src/endpoint.ts +++ b/packages/llm/src/endpoint.ts @@ -22,10 +22,9 @@ export interface EndpointInput { } /** - * Build a URL from the model's `baseURL` (or a default) plus a path. - * Honors `model.native.queryParams` so adapters that need request-level query - * params (Azure `api-version`, etc.) do not have to thread them through - * manually. + * Build a URL from the model's `baseURL` (or a default) plus a path. Appends + * `model.queryParams` so adapters that need request-level query params + * (Azure `api-version`, etc.) get them for free. * * Both `default` and `path` may be strings or functions of the * `EndpointInput`, for adapters whose URL embeds the model id, region, or @@ -43,7 +42,7 @@ export const baseURL = (input: { if (!base) return yield* ProviderShared.invalidRequest(input.required ?? "Missing baseURL") const path = typeof input.path === "string" ? input.path : input.path(ctx) const url = new URL(`${ProviderShared.trimBaseUrl(base)}${path}`) - const params = ProviderShared.queryParams(ctx.request) + const params = ctx.request.model.queryParams if (params) for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value) return url }) diff --git a/packages/llm/src/provider/openai-compatible-chat.ts b/packages/llm/src/provider/openai-compatible-chat.ts index bbc6172c84..5a260bca6f 100644 --- a/packages/llm/src/provider/openai-compatible-chat.ts +++ b/packages/llm/src/provider/openai-compatible-chat.ts @@ -38,22 +38,18 @@ export const adapter = Adapter.fromProtocol({ framing: Framing.sse, }) -export const model = (input: OpenAICompatibleChatModelInput) => { - const { queryParams, native, ...rest } = input - return llmModel({ - ...rest, +export const model = (input: OpenAICompatibleChatModelInput) => + llmModel({ + ...input, protocol: "openai-compatible-chat", - native: queryParams ? { ...native, queryParams } : native, capabilities: input.capabilities ?? capabilities({ tools: { calls: true, streamingInput: true } }), }) -} const familyModel = (family: ProviderFamily, input: ProviderFamilyModelInput) => model({ ...input, provider: family.provider, baseURL: input.baseURL ?? family.baseURL, - native: { ...input.native, openaiCompatibleProvider: family.provider }, }) export const baseten = (input: ProviderFamilyModelInput) => familyModel(families.baseten, input) diff --git a/packages/llm/src/provider/shared.ts b/packages/llm/src/provider/shared.ts index 8f37acb41e..6acb391055 100644 --- a/packages/llm/src/provider/shared.ts +++ b/packages/llm/src/provider/shared.ts @@ -119,15 +119,6 @@ export const mediaBytes = (part: MediaPart) => export const trimBaseUrl = (value: string) => value.replace(/\/+$/, "") -const isStringRecord = (value: unknown): value is Record => - isRecord(value) && Object.values(value).every((item) => typeof item === "string") - -export const queryParams = (request: { readonly model: { readonly native?: Record } }) => { - const value = request.model.native?.queryParams - if (!isStringRecord(value)) return undefined - return value -} - export const toolResultText = (part: ToolResultPart) => { if (part.result.type === "text" || part.result.type === "error") return String(part.result.value) return encodeJson(part.result.value) diff --git a/packages/llm/src/schema.ts b/packages/llm/src/schema.ts index 045dfaeaf3..04484c7e6f 100644 --- a/packages/llm/src/schema.ts +++ b/packages/llm/src/schema.ts @@ -84,8 +84,21 @@ export class ModelRef extends Schema.Class("LLM.ModelRef")({ */ apiKey: Schema.optional(Schema.String), headers: Schema.optional(Schema.Record(Schema.String, Schema.String)), + /** + * Query params appended to the request URL by `Endpoint.baseURL`. Used for + * deployment-level URL-scoped settings such as Azure's `api-version` or any + * provider that requires a per-request key in the URL. Generic concern, so + * lives as a typed first-class field instead of `native`. + */ + queryParams: Schema.optional(Schema.Record(Schema.String, Schema.String)), capabilities: ModelCapabilities, limits: ModelLimits, + /** + * Provider-specific opaque options. Reach for this only when the value is + * genuinely provider-private and does not fit a typed axis (e.g. Bedrock's + * `aws_credentials` / `aws_region` for SigV4). Anything used by more than + * one adapter should grow into a typed field instead. + */ native: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), }) {} diff --git a/packages/llm/test/provider/openai-chat.test.ts b/packages/llm/test/provider/openai-chat.test.ts index 1a55163b33..3b63a003c6 100644 --- a/packages/llm/test/provider/openai-chat.test.ts +++ b/packages/llm/test/provider/openai-chat.test.ts @@ -64,7 +64,7 @@ describe("OpenAI Chat adapter", () => { it.effect("adds native query params to the Chat Completions URL", () => Effect.gen(function* () { yield* LLMClient.make({ adapters: [OpenAIChat.adapter] }) - .generate(LLM.updateRequest(request, { model: LLM.model({ ...model, native: { queryParams: { "api-version": "v1" } } }) })) + .generate(LLM.updateRequest(request, { model: LLM.model({ ...model, queryParams: { "api-version": "v1" } }) })) .pipe( Effect.provide( dynamicResponse((input) => diff --git a/packages/llm/test/provider/openai-compatible-chat.test.ts b/packages/llm/test/provider/openai-compatible-chat.test.ts index 8cdcf52fb2..9c191a4ace 100644 --- a/packages/llm/test/provider/openai-compatible-chat.test.ts +++ b/packages/llm/test/provider/openai-compatible-chat.test.ts @@ -67,7 +67,7 @@ describe("OpenAI-compatible Chat adapter", () => { protocol: "openai-compatible-chat", baseURL: "https://api.deepseek.test/v1/", apiKey: "test-key", - native: { queryParams: { "api-version": "2026-01-01" } }, + queryParams: { "api-version": "2026-01-01" }, }) expect(prepared.target).toEqual({ model: "deepseek-chat", @@ -95,7 +95,6 @@ describe("OpenAI-compatible Chat adapter", () => { protocol: model.protocol, baseURL: model.baseURL, apiKey: model.apiKey, - native: model.native, } }), ).toEqual( @@ -105,7 +104,6 @@ describe("OpenAI-compatible Chat adapter", () => { protocol: "openai-compatible-chat", baseURL, apiKey: "test-key", - native: { openaiCompatibleProvider: provider }, })), ) @@ -118,7 +116,6 @@ describe("OpenAI-compatible Chat adapter", () => { provider: "deepseek", protocol: "openai-compatible-chat", baseURL: "https://custom.deepseek.test/v1", - native: { openaiCompatibleProvider: "deepseek" }, }) }), ) diff --git a/packages/llm/test/provider/openai-responses.test.ts b/packages/llm/test/provider/openai-responses.test.ts index 3a076a972a..255f622d62 100644 --- a/packages/llm/test/provider/openai-responses.test.ts +++ b/packages/llm/test/provider/openai-responses.test.ts @@ -45,7 +45,7 @@ describe("OpenAI Responses adapter", () => { it.effect("adds native query params to the Responses URL", () => Effect.gen(function* () { yield* LLMClient.make({ adapters: [OpenAIResponses.adapter] }) - .generate(LLM.updateRequest(request, { model: LLM.model({ ...model, native: { queryParams: { "api-version": "v1" } } }) })) + .generate(LLM.updateRequest(request, { model: LLM.model({ ...model, queryParams: { "api-version": "v1" } }) })) .pipe( Effect.provide( dynamicResponse((input) => diff --git a/packages/opencode/src/provider/llm-bridge.ts b/packages/opencode/src/provider/llm-bridge.ts index 4c3119293b..4be4f4249d 100644 --- a/packages/opencode/src/provider/llm-bridge.ts +++ b/packages/opencode/src/provider/llm-bridge.ts @@ -134,14 +134,9 @@ export const toModelRef = (input: Input): ModelRef | undefined => { baseURL: baseURL(input, resolution, options), apiKey: apiKey(input, resolution, options), headers: headers(input, options), + queryParams: resolution.queryParams, capabilities: capabilities(input, resolution), limits: LLM.limits({ context: input.model.limit.context, output: input.model.limit.output }), - native: { - opencodeProviderID: input.provider.id, - opencodeModelID: input.model.id, - npm: input.model.api.npm, - ...(resolution.queryParams ? { queryParams: resolution.queryParams } : {}), - }, }) } diff --git a/packages/opencode/src/session/llm-native.ts b/packages/opencode/src/session/llm-native.ts index 7bb98baece..037ae3c503 100644 --- a/packages/opencode/src/session/llm-native.ts +++ b/packages/opencode/src/session/llm-native.ts @@ -249,11 +249,7 @@ export const request = Effect.fn("LLMNative.request")(function* (input: RequestI toolChoice: input.toolChoice, generation: input.generation, metadata: input.metadata, - native: { - opencodeProviderID: input.provider.id, - opencodeModelID: input.model.id, - ...input.native, - }, + native: input.native, }) }) diff --git a/packages/opencode/test/provider/llm-bridge.test.ts b/packages/opencode/test/provider/llm-bridge.test.ts index 144388b6e2..8df0af8a26 100644 --- a/packages/opencode/test/provider/llm-bridge.test.ts +++ b/packages/opencode/test/provider/llm-bridge.test.ts @@ -134,7 +134,7 @@ describe("ProviderLLMBridge", () => { protocol: "openai-responses", baseURL: "https://opencode-test.openai.azure.com/openai/v1", apiKey: "azure-key", - native: { queryParams: { "api-version": "2025-04-01-preview" } }, + queryParams: { "api-version": "2025-04-01-preview" }, }) }) @@ -148,7 +148,7 @@ describe("ProviderLLMBridge", () => { provider: "azure", protocol: "openai-chat", baseURL: "https://opencode-test.openai.azure.com/openai/v1", - native: { queryParams: { "api-version": "v1" } }, + queryParams: { "api-version": "v1" }, }) }) diff --git a/packages/opencode/test/session/llm-native.test.ts b/packages/opencode/test/session/llm-native.test.ts index 76e2d77f9d..42a152184b 100644 --- a/packages/opencode/test/session/llm-native.test.ts +++ b/packages/opencode/test/session/llm-native.test.ts @@ -792,7 +792,7 @@ describe("LLMNative.request", () => { protocol: "openai-responses", baseURL: "https://opencode-test.openai.azure.com/openai/v1", apiKey: "azure-key", - native: { queryParams: { "api-version": "2025-04-01-preview" } }, + queryParams: { "api-version": "2025-04-01-preview" }, }) })) @@ -816,7 +816,7 @@ describe("LLMNative.request", () => { protocol: "openai-chat", baseURL: "https://opencode-test.openai.azure.com/openai/v1", apiKey: "azure-key", - native: { queryParams: { "api-version": "v1" } }, + queryParams: { "api-version": "v1" }, }) }))