diff --git a/packages/ai/AGENTS.md b/packages/ai/AGENTS.md index 94324648010..42bcfbec74e 100644 --- a/packages/ai/AGENTS.md +++ b/packages/ai/AGENTS.md @@ -12,7 +12,7 @@ Per-type constructors live on the type, not as top-level re-exports. Use `Message.system(...)`, `Message.user(...)`, `Message.assistant(...)`, `Message.tool(...)`, `LanguageModel.make(...)`, `ToolDefinition.make(...)`, `ToolCallPart.make(...)`, `ToolResultPart.make(...)`, `ToolChoice.make(...)`, `ToolChoice.named(...)`, `SystemPart.make(...)`, and `GenerationOptions.make(...)` directly. The top-level `LLM` namespace is reserved for request-shaped call APIs: `LLM.request`, `LLM.generate`, `LLM.stream`, and `LLM.generateObject`. Use `LLMRequest.update(...)` when deriving canonical request data; do not add a duplicate `LLM.updateRequest(...)` path. Two ways to construct the same thing is one too many. -- Keep provider-defined string enums forward-compatible. Expose known values for autocomplete while accepting future values with `Known | (string & {})`; use `Schema.String` at runtime unless rejecting unknown values is required for correctness. +- Prefer forward compatibility for provider-defined options that OpenCode only passes through. For pass-through string enums, expose known values for autocomplete while accepting future values with `Known | (string & {})`, and accept any string at runtime. Closed literals are appropriate when OpenCode branches on a value, transforms its associated structure, or otherwise cannot correctly handle an unknown variant. New options whose shape or behavior requires implementation remain unsupported until they are handled; do not blindly forward unknown structures. - Order reasoning-effort values from lowest to highest: `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. Provider-specific subsets follow the same relative order in types, schemas, option lists, and tests. ## Tests diff --git a/packages/ai/src/protocols/anthropic-messages.ts b/packages/ai/src/protocols/anthropic-messages.ts index e93168db8f3..e76be1dc50e 100644 --- a/packages/ai/src/protocols/anthropic-messages.ts +++ b/packages/ai/src/protocols/anthropic-messages.ts @@ -83,7 +83,7 @@ const AnthropicCacheControl = Schema.Struct({ type: Schema.tag("ephemeral"), ttl: Schema.optional(Schema.Literals(["5m", "1h"])), }) -const AnthropicServiceTier = Schema.Literals(["auto", "standard_only"]) +const AnthropicServiceTier = knownString<"auto" | "standard_only">() const AnthropicTextBlock = Schema.Struct({ type: Schema.tag("text"), @@ -276,7 +276,7 @@ const AnthropicThinkingBlockBinding = Schema.Struct({ }) const AnthropicThinkingFields = { - display: Schema.optional(Schema.Literals(["summarized", "omitted"])), + display: Schema.optional(knownString<"summarized" | "omitted">()), block_binding: Schema.optional(AnthropicThinkingBlockBinding), } const AnthropicThinkingEnabled = Schema.Struct({ diff --git a/packages/ai/test/provider-options/anthropic.types.ts b/packages/ai/test/provider-options/anthropic.types.ts index 6da715749f9..f5b5f183044 100644 --- a/packages/ai/test/provider-options/anthropic.types.ts +++ b/packages/ai/test/provider-options/anthropic.types.ts @@ -4,6 +4,22 @@ import { Anthropic } from "../../src/providers.js" const model = Anthropic.provider.model("claude-sonnet-4-5") LLM.request({ model, prompt: "Hello", providerOptions: { thinking: { type: "adaptive" } } }) +LLM.request({ + model, + prompt: "Hello", + providerOptions: { + serviceTier: "future-tier", + thinking: { type: "adaptive", display: "future-display" }, + }, +}) +LLM.request({ + model, + prompt: "Hello", + providerOptions: { + // @ts-expect-error Anthropic cache TTL values are protocol constraints. + cacheControl: { type: "ephemeral", ttl: "future-ttl" }, + }, +}) LLM.request({ model, diff --git a/packages/ai/test/provider/anthropic-messages.test.ts b/packages/ai/test/provider/anthropic-messages.test.ts index 6e5cd241f1f..4779b7b7b84 100644 --- a/packages/ai/test/provider/anthropic-messages.test.ts +++ b/packages/ai/test/provider/anthropic-messages.test.ts @@ -215,13 +215,31 @@ describe("Anthropic Messages route", () => { }), ) + it.effect("forwards unknown values for pass-through string enums", () => + Effect.gen(function* () { + const prepared = yield* compileRequest( + LLMRequest.update(request, { + providerOptions: { + service_tier: "future-tier", + thinking: { type: "adaptive", display: "future-display" }, + }, + }), + ) + + expect(prepared.body).toMatchObject({ + service_tier: "future-tier", + thinking: { type: "adaptive", display: "future-display" }, + }) + }), + ) + it.effect("ignores unknown provider options and rejects malformed known ones", () => Effect.gen(function* () { const prepared = yield* compileRequest(LLMRequest.update(request, { providerOptions: { unknownOption: true } })) const malformed = [ - { service_tier: "fast" }, + { service_tier: 1 }, { metadata: { user_id: 42 } }, - { cache_control: { type: "ephemeral", ttl: "2h" } }, + { cache_control: { type: "ephemeral", ttl: "future-ttl" } }, { output_config: { format: { type: "text" } } }, { thinking: { type: "automatic" } }, ]