diff --git a/packages/ai/src/protocols/anthropic-messages.ts b/packages/ai/src/protocols/anthropic-messages.ts index db0a0e34a1..b3a35f986e 100644 --- a/packages/ai/src/protocols/anthropic-messages.ts +++ b/packages/ai/src/protocols/anthropic-messages.ts @@ -13,6 +13,7 @@ import { type JsonSchema, type LLMRequest, type MediaPart, + type ProviderOptions, type ProviderMetadata, type ToolCallPart, type ToolDefinition, @@ -22,7 +23,6 @@ import { import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared" import { classifyProviderFailure } from "../provider-error" import * as Cache from "./utils/cache" -import { AnthropicOptions } from "./utils/anthropic-options" import { Lifecycle } from "./utils/lifecycle" import { ToolSchemaProjection } from "./utils/tool-schema" import { ToolStream } from "./utils/tool-stream" @@ -32,6 +32,29 @@ const MEDIA_MIMES = new Set([...ProviderShared.IMAGE_MIMES, ...ProviderS export const DEFAULT_BASE_URL = "https://api.anthropic.com/v1" export const PATH = "/messages" +export type ThinkingInput = + | { + readonly type: "adaptive" + readonly display?: "summarized" | "omitted" + } + | { + readonly type: "disabled" + } + | ({ readonly type: "enabled" } & ( + | { readonly budgetTokens: number; readonly budget_tokens?: number } + | { readonly budgetTokens?: number; readonly budget_tokens: number } + )) + +export interface OptionsInput { + readonly [key: string]: unknown + readonly thinking?: ThinkingInput + readonly effort?: string +} + +export type ProviderOptionsInput = ProviderOptions & { + readonly anthropic?: OptionsInput +} + // ============================================================================= // Request Body Schema // ============================================================================= @@ -174,6 +197,20 @@ const AnthropicToolChoice = Schema.Union([ Schema.Struct({ type: Schema.tag("tool"), name: Schema.String }), ]) +const AnthropicThinking = Schema.Union([ + Schema.Struct({ + type: Schema.tag("enabled"), + budget_tokens: Schema.Number, + }), + Schema.Struct({ + type: Schema.tag("adaptive"), + display: Schema.optional(Schema.Literals(["summarized", "omitted"])), + }), + Schema.Struct({ + type: Schema.tag("disabled"), + }), +]) + const AnthropicOutputConfig = Schema.Struct({ effort: Schema.optional(Schema.String), }) @@ -190,7 +227,7 @@ const AnthropicBodyFields = { top_p: Schema.optional(Schema.Number), top_k: Schema.optional(Schema.Number), stop_sequences: optionalArray(Schema.String), - thinking: Schema.optional(AnthropicOptions.ThinkingSchema), + thinking: Schema.optional(AnthropicThinking), output_config: Schema.optional(AnthropicOutputConfig), } export const AnthropicMessagesBody = Schema.Struct(AnthropicBodyFields) @@ -524,6 +561,38 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* ( return messages }) +const resolveOptions = Effect.fn("AnthropicMessages.resolveOptions")(function* (request: LLMRequest) { + const input = request.providerOptions?.anthropic + return { + thinking: yield* resolveThinking(input?.thinking), + effort: typeof input?.effort === "string" ? input.effort : undefined, + } +}) + +const resolveThinking = Effect.fn("AnthropicMessages.resolveThinking")(function* (input: unknown) { + if (!ProviderShared.isRecord(input)) return undefined + if (input.type === "adaptive") { + const display = + input.display === "summarized" + ? ("summarized" as const) + : input.display === "omitted" + ? ("omitted" as const) + : undefined + return { type: "adaptive" as const, ...(display === undefined ? {} : { display }) } + } + if (input.type === "disabled") return { type: "disabled" as const } + if (input.type !== "enabled") return undefined + const budget = + typeof input.budgetTokens === "number" + ? input.budgetTokens + : typeof input.budget_tokens === "number" + ? input.budget_tokens + : undefined + if (budget === undefined) + return yield* ProviderShared.invalidRequest("Anthropic thinking provider option requires budgetTokens") + return { type: "enabled" as const, budget_tokens: budget } +}) + const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (request: LLMRequest) { const generation = request.generation const toolSchemaCompatibility = request.model.compatibility?.toolSchema @@ -558,7 +627,7 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques `Anthropic Messages: dropped ${breakpoints.dropped} cache breakpoint(s); the API allows at most ${ANTHROPIC_BREAKPOINT_CAP} per request.`, ) } - const options = yield* AnthropicOptions.resolve(request) + const options = yield* resolveOptions(request) return { model: request.model.id, system, diff --git a/packages/ai/src/protocols/gemini.ts b/packages/ai/src/protocols/gemini.ts index 48eca6f599..8a62d50fb8 100644 --- a/packages/ai/src/protocols/gemini.ts +++ b/packages/ai/src/protocols/gemini.ts @@ -11,6 +11,7 @@ import { type JsonSchema, type LLMRequest, type MediaPart, + type ProviderOptions, type ProviderMetadata, type TextPart, type ToolCallPart, @@ -18,7 +19,6 @@ import { type ToolContent, } from "../schema" import { JsonObject, optionalArray, ProviderShared } from "./shared" -import { GeminiOptions } from "./utils/gemini-options" import { GeminiToolSchema } from "./utils/gemini-tool-schema" import { Lifecycle } from "./utils/lifecycle" import { ToolSchemaProjection } from "./utils/tool-schema" @@ -27,6 +27,18 @@ const ADAPTER = "gemini" const MEDIA_MIMES = new Set(ProviderShared.MEDIA_MIMES) export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta" +export interface OptionsInput { + readonly [key: string]: unknown + readonly thinkingConfig?: { + readonly thinkingBudget?: number + readonly includeThoughts?: boolean + } +} + +export type ProviderOptionsInput = ProviderOptions & { + readonly gemini?: OptionsInput +} + // ============================================================================= // Request Body Schema // ============================================================================= @@ -96,13 +108,18 @@ const GeminiToolConfig = Schema.Struct({ }), }) +const GeminiThinkingConfig = Schema.Struct({ + thinkingBudget: Schema.optional(Schema.Number), + includeThoughts: Schema.optional(Schema.Boolean), +}) + const GeminiGenerationConfig = Schema.Struct({ maxOutputTokens: Schema.optional(Schema.Number), temperature: Schema.optional(Schema.Number), topP: Schema.optional(Schema.Number), topK: Schema.optional(Schema.Number), stopSequences: optionalArray(Schema.String), - thinkingConfig: Schema.optional(GeminiOptions.ThinkingConfigSchema), + thinkingConfig: Schema.optional(GeminiThinkingConfig), }) const GeminiBodyFields = { @@ -298,10 +315,22 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR return contents }) +const resolveOptions = (request: LLMRequest) => { + const value = request.providerOptions?.gemini?.thinkingConfig + if (!ProviderShared.isRecord(value)) return {} + const thinkingConfig = { + thinkingBudget: typeof value.thinkingBudget === "number" ? value.thinkingBudget : undefined, + includeThoughts: typeof value.includeThoughts === "boolean" ? value.includeThoughts : undefined, + } + return { + thinkingConfig: Object.values(thinkingConfig).some((item) => item !== undefined) ? thinkingConfig : undefined, + } +} + const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request: LLMRequest) { const hasTools = request.tools.length > 0 const generation = request.generation - const options = GeminiOptions.resolve(request) + const options = resolveOptions(request) const toolSchemaCompatibility = request.model.compatibility?.toolSchema const generationConfig = { maxOutputTokens: generation?.maxTokens, diff --git a/packages/ai/src/protocols/utils/anthropic-options.ts b/packages/ai/src/protocols/utils/anthropic-options.ts deleted file mode 100644 index 63ee00b70c..0000000000 --- a/packages/ai/src/protocols/utils/anthropic-options.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Effect, Schema } from "effect" -import type { LLMRequest } from "../../schema" -import { ProviderShared } from "../shared" - -export const ThinkingSchema = Schema.Union([ - Schema.Struct({ - type: Schema.tag("enabled"), - budget_tokens: Schema.Number, - }), - Schema.Struct({ - type: Schema.tag("adaptive"), - display: Schema.optional(Schema.Literals(["summarized", "omitted"])), - }), - Schema.Struct({ - type: Schema.tag("disabled"), - }), -]) -export type Thinking = Schema.Schema.Type - -export interface Resolved { - readonly thinking?: Thinking - readonly effort?: string -} - -export const resolve = Effect.fn("AnthropicOptions.resolve")(function* (request: LLMRequest) { - const input = request.providerOptions?.anthropic - return { - thinking: yield* resolveThinking(input?.thinking), - effort: typeof input?.effort === "string" ? input.effort : undefined, - } satisfies Resolved -}) - -const resolveThinking = Effect.fn("AnthropicOptions.resolveThinking")(function* (input: unknown) { - if (!ProviderShared.isRecord(input)) return undefined - if (input.type === "adaptive") { - const display = - input.display === "summarized" - ? ("summarized" as const) - : input.display === "omitted" - ? ("omitted" as const) - : undefined - return { type: "adaptive" as const, ...(display === undefined ? {} : { display }) } - } - if (input.type === "disabled") return { type: "disabled" as const } - if (input.type !== "enabled") return undefined - const budget = - typeof input.budgetTokens === "number" - ? input.budgetTokens - : typeof input.budget_tokens === "number" - ? input.budget_tokens - : undefined - if (budget === undefined) - return yield* ProviderShared.invalidRequest("Anthropic thinking provider option requires budgetTokens") - return { type: "enabled" as const, budget_tokens: budget } -}) - -export * as AnthropicOptions from "./anthropic-options" diff --git a/packages/ai/src/protocols/utils/gemini-options.ts b/packages/ai/src/protocols/utils/gemini-options.ts deleted file mode 100644 index f4828a00a4..0000000000 --- a/packages/ai/src/protocols/utils/gemini-options.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Schema } from "effect" -import type { LLMRequest } from "../../schema" -import { ProviderShared } from "../shared" - -export const ThinkingConfigSchema = Schema.Struct({ - thinkingBudget: Schema.optional(Schema.Number), - includeThoughts: Schema.optional(Schema.Boolean), -}) -export type ThinkingConfig = Schema.Schema.Type - -export interface Resolved { - readonly thinkingConfig?: ThinkingConfig -} - -export const resolve = (request: LLMRequest): Resolved => { - const value = request.providerOptions?.gemini?.thinkingConfig - if (!ProviderShared.isRecord(value)) return {} - const thinkingConfig = { - thinkingBudget: typeof value.thinkingBudget === "number" ? value.thinkingBudget : undefined, - includeThoughts: typeof value.includeThoughts === "boolean" ? value.includeThoughts : undefined, - } - return { - thinkingConfig: Object.values(thinkingConfig).some((item) => item !== undefined) ? thinkingConfig : undefined, - } -} - -export * as GeminiOptions from "./gemini-options" diff --git a/packages/ai/src/providers/anthropic-compatible.ts b/packages/ai/src/providers/anthropic-compatible.ts index 50867ebb44..56cc7cee32 100644 --- a/packages/ai/src/providers/anthropic-compatible.ts +++ b/packages/ai/src/providers/anthropic-compatible.ts @@ -4,9 +4,10 @@ import { Auth } from "../route/auth" import type { ProviderAuthOption } from "../route/auth-options" import type { RouteDefaultsInput } from "../route/client" import { ProviderID, type ModelID } from "../schema" -import type { AnthropicProviderOptionsInput } from "./anthropic-options" -export type { AnthropicOptionsInput, AnthropicProviderOptionsInput, AnthropicThinkingInput } from "./anthropic-options" +export type AnthropicOptionsInput = AnthropicMessages.OptionsInput +export type AnthropicProviderOptionsInput = AnthropicMessages.ProviderOptionsInput +export type AnthropicThinkingInput = AnthropicMessages.ThinkingInput export const id = ProviderID.make("anthropic-compatible") @@ -14,7 +15,7 @@ export type Config = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly provider?: string readonly baseURL: string - readonly providerOptions?: AnthropicProviderOptionsInput + readonly providerOptions?: AnthropicMessages.ProviderOptionsInput } export type Settings = ProviderPackage.Settings & @@ -24,7 +25,7 @@ export type Settings = ProviderPackage.Settings & ) & { readonly baseURL: string readonly provider?: string - readonly providerOptions?: AnthropicProviderOptionsInput + readonly providerOptions?: AnthropicMessages.ProviderOptionsInput } export const routes = [AnthropicMessages.route] diff --git a/packages/ai/src/providers/anthropic-options.ts b/packages/ai/src/providers/anthropic-options.ts deleted file mode 100644 index 61f81cc074..0000000000 --- a/packages/ai/src/providers/anthropic-options.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { ProviderOptions } from "../schema" - -export type AnthropicThinkingInput = - | { - readonly type: "adaptive" - readonly display?: "summarized" | "omitted" - } - | { - readonly type: "disabled" - } - | ({ readonly type: "enabled" } & ( - | { readonly budgetTokens: number; readonly budget_tokens?: number } - | { readonly budgetTokens?: number; readonly budget_tokens: number } - )) - -export interface AnthropicOptionsInput { - readonly [key: string]: unknown - readonly thinking?: AnthropicThinkingInput - readonly effort?: string -} - -export type AnthropicProviderOptionsInput = ProviderOptions & { - readonly anthropic?: AnthropicOptionsInput -} - -export * as AnthropicProviderOptions from "./anthropic-options" diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index 5eedb63ac3..eb175ec50c 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -5,9 +5,10 @@ import type { ProviderPackage } from "../provider-package" import { ProviderID, type ModelID } from "../schema" import { AnthropicMessages } from "../protocols/anthropic-messages" import { AnthropicCompatible } from "./anthropic-compatible" -import type { AnthropicProviderOptionsInput } from "./anthropic-options" -export type { AnthropicOptionsInput, AnthropicProviderOptionsInput, AnthropicThinkingInput } from "./anthropic-options" +export type AnthropicOptionsInput = AnthropicMessages.OptionsInput +export type AnthropicProviderOptionsInput = AnthropicMessages.ProviderOptionsInput +export type AnthropicThinkingInput = AnthropicMessages.ThinkingInput export const id = ProviderID.make("anthropic") @@ -16,7 +17,7 @@ export const routes = [AnthropicMessages.route] export type Config = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly baseURL?: string - readonly providerOptions?: AnthropicProviderOptionsInput + readonly providerOptions?: AnthropicMessages.ProviderOptionsInput } export type Settings = ProviderPackage.Settings & @@ -25,7 +26,7 @@ export type Settings = ProviderPackage.Settings & | { readonly apiKey?: never; readonly authToken?: string } ) & { readonly baseURL?: string - readonly providerOptions?: AnthropicProviderOptionsInput + readonly providerOptions?: AnthropicMessages.ProviderOptionsInput } const auth = (options: ProviderAuthOption<"optional">) => { diff --git a/packages/ai/src/providers/gemini-options.ts b/packages/ai/src/providers/gemini-options.ts deleted file mode 100644 index 067c38e144..0000000000 --- a/packages/ai/src/providers/gemini-options.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { ThinkingConfig } from "../protocols/utils/gemini-options" -import type { ProviderOptions } from "../schema" - -export interface GeminiOptionsInput { - readonly [key: string]: unknown - readonly thinkingConfig?: ThinkingConfig -} - -export type GeminiProviderOptionsInput = ProviderOptions & { - readonly gemini?: GeminiOptionsInput -} - -export * as GeminiProviderOptions from "./gemini-options" diff --git a/packages/ai/src/providers/google-vertex-messages.ts b/packages/ai/src/providers/google-vertex-messages.ts index b52eb94f93..53c4e16b0d 100644 --- a/packages/ai/src/providers/google-vertex-messages.ts +++ b/packages/ai/src/providers/google-vertex-messages.ts @@ -7,10 +7,11 @@ import { Endpoint } from "../route/endpoint" import { Framing } from "../route/framing" import { Protocol } from "../route/protocol" import { ProviderID, type ModelID } from "../schema" -import type { AnthropicProviderOptionsInput } from "./anthropic-options" import { GoogleVertexShared } from "./google-vertex-shared" -export type { AnthropicOptionsInput, AnthropicProviderOptionsInput, AnthropicThinkingInput } from "./anthropic-options" +export type AnthropicOptionsInput = AnthropicMessages.OptionsInput +export type AnthropicProviderOptionsInput = AnthropicMessages.ProviderOptionsInput +export type AnthropicThinkingInput = AnthropicMessages.ThinkingInput const VERSION = "vertex-2023-10-16" as const @@ -22,7 +23,7 @@ export type Config = RouteDefaultsInput & readonly baseURL?: string readonly location?: string readonly project?: string - readonly providerOptions?: AnthropicProviderOptionsInput + readonly providerOptions?: AnthropicMessages.ProviderOptionsInput } export interface Settings extends ProviderPackage.Settings { @@ -31,7 +32,7 @@ export interface Settings extends ProviderPackage.Settings { readonly baseURL?: string readonly location?: string readonly project?: string - readonly providerOptions?: AnthropicProviderOptionsInput + readonly providerOptions?: AnthropicMessages.ProviderOptionsInput } const route = Route.make({ diff --git a/packages/ai/src/providers/google-vertex.ts b/packages/ai/src/providers/google-vertex.ts index f7994fb16d..78f4e0764d 100644 --- a/packages/ai/src/providers/google-vertex.ts +++ b/packages/ai/src/providers/google-vertex.ts @@ -5,10 +5,10 @@ import { Route, type RouteDefaultsInput } from "../route/client" import { Endpoint } from "../route/endpoint" import { Framing } from "../route/framing" import { ProviderID, type ModelID } from "../schema" -import type { GeminiProviderOptionsInput } from "./gemini-options" import { GoogleVertexShared } from "./google-vertex-shared" -export type { GeminiOptionsInput, GeminiProviderOptionsInput } from "./gemini-options" +export type GeminiOptionsInput = Gemini.OptionsInput +export type GeminiProviderOptionsInput = Gemini.ProviderOptionsInput export const id = ProviderID.make("google-vertex") @@ -17,7 +17,7 @@ export type Config = RouteDefaultsInput & readonly baseURL?: string readonly location?: string readonly project?: string - readonly providerOptions?: GeminiProviderOptionsInput + readonly providerOptions?: Gemini.ProviderOptionsInput } export type Settings = ProviderPackage.Settings & @@ -28,7 +28,7 @@ export type Settings = ProviderPackage.Settings & readonly baseURL?: string readonly location?: string readonly project?: string - readonly providerOptions?: GeminiProviderOptionsInput + readonly providerOptions?: Gemini.ProviderOptionsInput } const route = Route.make({ diff --git a/packages/ai/src/providers/google.ts b/packages/ai/src/providers/google.ts index 0199d594e6..2c3476117c 100644 --- a/packages/ai/src/providers/google.ts +++ b/packages/ai/src/providers/google.ts @@ -5,10 +5,10 @@ import type { ProviderPackage } from "../provider-package" import { HttpOptions, ProviderID, mergeHttpOptions, type ModelID } from "../schema" import { Gemini } from "../protocols/gemini" import { GoogleImages } from "../protocols/google-images" -import type { GeminiProviderOptionsInput } from "./gemini-options" export type { GoogleImageOptions } from "../protocols/google-images" -export type { GeminiOptionsInput, GeminiProviderOptionsInput } from "./gemini-options" +export type GeminiOptionsInput = Gemini.OptionsInput +export type GeminiProviderOptionsInput = Gemini.ProviderOptionsInput export const id = ProviderID.make("google") @@ -17,13 +17,13 @@ export const routes = [Gemini.route] export type Config = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly baseURL?: string - readonly providerOptions?: GeminiProviderOptionsInput + readonly providerOptions?: Gemini.ProviderOptionsInput } export interface Settings extends ProviderPackage.Settings { readonly apiKey?: string readonly baseURL?: string - readonly providerOptions?: GeminiProviderOptionsInput + readonly providerOptions?: Gemini.ProviderOptionsInput } const auth = (options: ProviderAuthOption<"optional">) => {