@opencode/ai 0.0.0-dev-20587 → 0.0.0-dev-20592
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cache-policy.js +12 -9
- package/dist/protocols/utils/gateway-protocol.d.ts +17 -0
- package/dist/protocols/utils/gateway-protocol.js +57 -0
- package/dist/providers/vercel-ai-gateway/chat.d.ts +2 -0
- package/dist/providers/vercel-ai-gateway/chat.js +1 -0
- package/dist/providers/vercel-ai-gateway/messages.d.ts +2 -0
- package/dist/providers/vercel-ai-gateway/messages.js +1 -0
- package/dist/providers/vercel-ai-gateway/responses.d.ts +2 -0
- package/dist/providers/vercel-ai-gateway/responses.js +1 -0
- package/dist/providers/vercel-ai-gateway.d.ts +91 -9
- package/dist/providers/vercel-ai-gateway.js +161 -21
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1283,7 +1283,7 @@ const gateway = CloudflareAIGateway.configure({
|
|
|
1283
1283
|
}).model("workers-ai/@cf/meta/llama-3.1-8b-instruct")
|
|
1284
1284
|
```
|
|
1285
1285
|
|
|
1286
|
-
Included LLM providers: OpenAI, Anthropic, Google (Gemini), Google Vertex, Amazon Bedrock, Azure OpenAI, Baseten, Cerebras, Cohere, Cloudflare AI Gateway, Cloudflare Workers AI, DeepInfra, DeepSeek, Fireworks, Groq, Mistral, OpenRouter, TogetherAI, and xAI. Z.ai currently exposes image generation. Generic Chat Completions, Responses, and Anthropic Messages-compatible entrypoints support custom endpoints.
|
|
1286
|
+
Included LLM providers: OpenAI, Anthropic, Google (Gemini), Google Vertex, Amazon Bedrock, Azure OpenAI, Baseten, Cerebras, Cohere, Cloudflare AI Gateway, Cloudflare Workers AI, DeepInfra, DeepSeek, Fireworks, Groq, Mistral, OpenRouter, TogetherAI, Vercel AI Gateway, and xAI. Z.ai currently exposes image generation. Generic Chat Completions, Responses, and Anthropic Messages-compatible entrypoints support custom endpoints.
|
|
1287
1287
|
|
|
1288
1288
|
Each named provider owns its module, endpoint, authentication, and route setup. Providers with the same wire format compose the shared protocol directly:
|
|
1289
1289
|
|
package/dist/cache-policy.js
CHANGED
|
@@ -46,21 +46,22 @@ const RESPECTS_INLINE_HINTS = new Set([
|
|
|
46
46
|
"meta-messages",
|
|
47
47
|
"minimax-messages",
|
|
48
48
|
"moonshot-messages",
|
|
49
|
+
"vercel-ai-gateway-messages",
|
|
49
50
|
"zai-coding-messages",
|
|
50
51
|
"bedrock-converse",
|
|
51
52
|
"openrouter",
|
|
52
53
|
"digitalocean",
|
|
53
54
|
]);
|
|
54
|
-
// OpenRouter upstreams other than Anthropic and Alibaba Qwen cache without breakpoints.
|
|
55
|
-
// breakpoint, so a conversation-tail breakpoint writes a new cache every step and costs
|
|
56
|
-
// breakpoints on tool definitions and caches tools with the system prompt.
|
|
55
|
+
// OpenRouter and Vercel AI Gateway upstreams other than Anthropic and Alibaba Qwen cache without breakpoints.
|
|
56
|
+
// Gemini uses only the last breakpoint, so a conversation-tail breakpoint writes a new cache every step and costs
|
|
57
|
+
// more than none. Qwen ignores breakpoints on tool definitions and caches tools with the system prompt.
|
|
57
58
|
const QWEN = { system: true, messages: { tail: 1 } };
|
|
58
|
-
const
|
|
59
|
+
const gatewayPolicy = (modelID) => {
|
|
59
60
|
// `~anthropic/claude-sonnet-latest` style IDs are OpenRouter aliases for the latest model in a family.
|
|
60
61
|
const id = modelID.replace(/^~/, "");
|
|
61
62
|
if (id.startsWith("anthropic/"))
|
|
62
63
|
return AUTO;
|
|
63
|
-
if (id.startsWith("qwen/"))
|
|
64
|
+
if (id.startsWith("qwen/") || id.startsWith("alibaba/qwen"))
|
|
64
65
|
return QWEN;
|
|
65
66
|
return NONE;
|
|
66
67
|
};
|
|
@@ -142,11 +143,13 @@ const countHints = (request) => countToolHints(request.tools) +
|
|
|
142
143
|
request.messages.reduce((count, message) => count +
|
|
143
144
|
message.content.reduce((contentCount, part) => contentCount + ("cache" in part && part.cache !== undefined ? 1 : 0), 0), 0);
|
|
144
145
|
export const applyCachePolicy = (request) => {
|
|
145
|
-
|
|
146
|
+
const route = request.model.route.id;
|
|
147
|
+
if (!RESPECTS_INLINE_HINTS.has(route))
|
|
146
148
|
return request;
|
|
147
|
-
const policy =
|
|
148
|
-
|
|
149
|
-
|
|
149
|
+
const policy = (route === "openrouter" || route === "vercel-ai-gateway-messages") &&
|
|
150
|
+
(request.cache === undefined || request.cache === "auto")
|
|
151
|
+
? gatewayPolicy(request.model.id)
|
|
152
|
+
: route === "alibaba-chat" && (request.cache === undefined || request.cache === "auto")
|
|
150
153
|
? request.model.id.toLowerCase().startsWith("qwen")
|
|
151
154
|
? QWEN
|
|
152
155
|
: NONE
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { Protocol } from "../../route/protocol.js";
|
|
3
|
+
import { type AIError, type LLMRequest } from "../../schema/index.js";
|
|
4
|
+
interface ParserState<Inner> {
|
|
5
|
+
readonly inner: Inner;
|
|
6
|
+
readonly gateway?: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
export declare function gatewayProtocol<Body, Event, State>(protocol: Protocol<Body, string, Event, State>, input: {
|
|
9
|
+
readonly id: string;
|
|
10
|
+
readonly prepare: (request: LLMRequest) => Effect.Effect<{
|
|
11
|
+
readonly request: LLMRequest;
|
|
12
|
+
readonly body: Record<string, unknown>;
|
|
13
|
+
}, AIError>;
|
|
14
|
+
}): Protocol<{
|
|
15
|
+
readonly [x: string]: unknown;
|
|
16
|
+
}, string, Event, ParserState<State>>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Effect, Option, Schema } from "effect";
|
|
2
|
+
import { Protocol } from "../../route/protocol.js";
|
|
3
|
+
import { LLMEvent, mergeJsonRecords } from "../../schema/index.js";
|
|
4
|
+
import { JsonObject, lenient } from "../shared.js";
|
|
5
|
+
const GatewayHolder = Schema.Struct({
|
|
6
|
+
provider_metadata: lenient(Schema.Struct({
|
|
7
|
+
gateway: lenient(JsonObject),
|
|
8
|
+
})),
|
|
9
|
+
});
|
|
10
|
+
const GatewayEvent = Schema.Struct({
|
|
11
|
+
...GatewayHolder.fields,
|
|
12
|
+
response: lenient(GatewayHolder),
|
|
13
|
+
choices: lenient(Schema.Array(Schema.Struct({
|
|
14
|
+
delta: lenient(GatewayHolder),
|
|
15
|
+
}))),
|
|
16
|
+
});
|
|
17
|
+
const decodeGatewayEvent = Schema.decodeUnknownOption(GatewayEvent);
|
|
18
|
+
function attachGatewayMetadata(events, gateway) {
|
|
19
|
+
if (!gateway || !events.some(LLMEvent.is.finish))
|
|
20
|
+
return events;
|
|
21
|
+
return events.map((event) => LLMEvent.is.finish(event) ? { ...event, providerMetadata: { ...event.providerMetadata, gateway } } : event);
|
|
22
|
+
}
|
|
23
|
+
export function gatewayProtocol(protocol, input) {
|
|
24
|
+
const initial = (request) => ({
|
|
25
|
+
inner: protocol.stream.initial(request),
|
|
26
|
+
});
|
|
27
|
+
const onHalt = protocol.stream.onHalt;
|
|
28
|
+
return Protocol.make({
|
|
29
|
+
id: input.id,
|
|
30
|
+
body: {
|
|
31
|
+
schema: JsonObject,
|
|
32
|
+
from: Effect.fnUntraced(function* (request) {
|
|
33
|
+
const prepared = yield* input.prepare(request);
|
|
34
|
+
const body = yield* protocol.body.from(prepared.request);
|
|
35
|
+
return { ...body, ...prepared.body };
|
|
36
|
+
}),
|
|
37
|
+
},
|
|
38
|
+
supportsEffortUpdates: protocol.supportsEffortUpdates,
|
|
39
|
+
sanitizer: protocol.sanitizer,
|
|
40
|
+
stream: {
|
|
41
|
+
event: protocol.stream.event,
|
|
42
|
+
initial,
|
|
43
|
+
step: Effect.fnUntraced(function* (state, event) {
|
|
44
|
+
const decoded = Option.getOrUndefined(decodeGatewayEvent(event));
|
|
45
|
+
const gateway = decoded
|
|
46
|
+
? mergeJsonRecords(state.gateway, decoded.provider_metadata?.gateway, decoded.response?.provider_metadata?.gateway, ...(decoded.choices ?? []).map((choice) => choice.delta?.provider_metadata?.gateway))
|
|
47
|
+
: state.gateway;
|
|
48
|
+
const [inner, events] = yield* protocol.stream.step(state.inner, event);
|
|
49
|
+
return [{ inner, gateway }, attachGatewayMetadata(events, gateway)];
|
|
50
|
+
}),
|
|
51
|
+
terminal: protocol.stream.terminal,
|
|
52
|
+
onHalt: onHalt
|
|
53
|
+
? (state) => onHalt(state.inner).pipe(Effect.map((events) => attachGatewayMetadata(events, state.gateway)))
|
|
54
|
+
: undefined,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { chatModel as model } from "../vercel-ai-gateway.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { messagesModel as model } from "../vercel-ai-gateway.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { responsesModel as model } from "../vercel-ai-gateway.js";
|
|
@@ -1,22 +1,89 @@
|
|
|
1
1
|
import { EvaluationModel } from "../experimental/evaluation.js";
|
|
2
|
+
import { AnthropicMessages } from "../protocols/anthropic-messages.js";
|
|
3
|
+
import type { ProviderPackage } from "../provider-package.js";
|
|
2
4
|
import { type ProviderAuthOption } from "../route/auth-options.js";
|
|
3
|
-
import {
|
|
5
|
+
import { Route, type RouteDefaultsInput } from "../route/client.js";
|
|
6
|
+
import { ModelID } from "../schema/index.js";
|
|
7
|
+
import type { OpenResponsesProviderOptionsInput } from "./open-responses-options.js";
|
|
4
8
|
export declare const id: string & import("effect/Brand").Brand<"AI.ProviderID">;
|
|
5
|
-
export interface
|
|
9
|
+
export interface GatewayOptions {
|
|
10
|
+
/** Service-owned options added by the Gateway without requiring an SDK update. */
|
|
6
11
|
readonly [key: string]: unknown;
|
|
7
|
-
|
|
12
|
+
/** Enables Gateway automatic prompt-cache breakpoint injection (`"auto"`). */
|
|
13
|
+
readonly caching?: "auto" | (string & {});
|
|
14
|
+
/** Provider slugs that are the only ones allowed to serve the request (e.g. `["anthropic", "vertex"]`). */
|
|
15
|
+
readonly only?: ReadonlyArray<string>;
|
|
16
|
+
/** Provider slugs specifying the order in which providers are tried (e.g. `["bedrock", "anthropic"]`). */
|
|
17
|
+
readonly order?: ReadonlyArray<string>;
|
|
18
|
+
/** Sort candidate providers by cost (`"cost"`), throughput (`"tps"`), or time-to-first-token (`"ttft"`). */
|
|
19
|
+
readonly sort?: "cost" | "tps" | "ttft" | (string & {});
|
|
20
|
+
/** Fallback models to try in order, or a conditional `{ model, when }` entry on evaluation requests. */
|
|
21
|
+
readonly models?: ReadonlyArray<string | Readonly<Record<string, unknown>>>;
|
|
22
|
+
/** Restrict routing to providers with zero data retention agreements. */
|
|
23
|
+
readonly zeroDataRetention?: boolean;
|
|
24
|
+
/** Restrict routing to providers that do not train on prompt data. */
|
|
25
|
+
readonly disallowPromptTraining?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Restrict routing to provider models that satisfy every entry: capability
|
|
28
|
+
* tags (`"implicit-caching"`, `"reasoning"`, `"structured-output"`,
|
|
29
|
+
* `"tool-use"`, `"vision"`) or weight-format filters (`"quantization:fp8"`,
|
|
30
|
+
* `"!quantization:fp8"`).
|
|
31
|
+
*/
|
|
32
|
+
readonly has?: ReadonlyArray<"implicit-caching" | "reasoning" | "structured-output" | "tool-use" | "vision" | `quantization:${string}` | `!quantization:${string}` | (string & {})>;
|
|
33
|
+
/** Entity identifier against which Gateway quota is tracked. */
|
|
34
|
+
readonly quotaEntityId?: string;
|
|
35
|
+
/** Unified service tier intent (`"flex"` or `"priority"`). */
|
|
36
|
+
readonly serviceTier?: "flex" | "priority" | (string & {});
|
|
37
|
+
/** End-user identifier for spend tracking and attribution. */
|
|
38
|
+
readonly user?: string;
|
|
39
|
+
/** User-specified tags for reporting and filtering usage. */
|
|
40
|
+
readonly tags?: ReadonlyArray<string>;
|
|
41
|
+
/** Request-scoped BYOK credentials keyed by provider slug, used instead of cached workspace credentials. */
|
|
42
|
+
readonly byok?: Readonly<Record<string, ReadonlyArray<Readonly<Record<string, unknown>>>>>;
|
|
43
|
+
/** Preferred inference region for upstream provider routing. */
|
|
44
|
+
readonly inferenceRegion?: string;
|
|
45
|
+
/** Per-provider timeouts in milliseconds (e.g. `{ byok: { anthropic: 3000 } }`). */
|
|
46
|
+
readonly providerTimeouts?: {
|
|
8
47
|
readonly [key: string]: unknown;
|
|
9
|
-
readonly
|
|
10
|
-
|
|
11
|
-
|
|
48
|
+
readonly byok?: Readonly<Record<string, number>>;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export type ProviderOptionsInput = OpenResponsesProviderOptionsInput & Omit<AnthropicMessages.OptionsInput, "thinking"> & {
|
|
52
|
+
/** Reasoning configuration for Messages (`thinking`) or Chat (`reasoning.enabled` + `reasoning.max_tokens`). */
|
|
53
|
+
readonly thinking?: AnthropicMessages.OptionsInput["thinking"] | {
|
|
54
|
+
readonly type: "enabled" | "adaptive" | "disabled" | (string & {});
|
|
55
|
+
readonly budgetTokens?: number;
|
|
56
|
+
readonly budget_tokens?: number;
|
|
57
|
+
};
|
|
58
|
+
/** Gateway routing, fallback, BYOK, compliance, and attribution options sent under `body.providerOptions.gateway`. */
|
|
59
|
+
readonly gateway?: GatewayOptions;
|
|
60
|
+
/** Provider-specific options forwarded under their upstream namespace in `body.providerOptions` (e.g. `{ anthropic: { ... } }`). */
|
|
61
|
+
readonly upstream?: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
|
|
62
|
+
/** Responses API automatic-cache lifetime (`"5m"` or `"1h"`), sent as top-level `cache_ttl`. */
|
|
63
|
+
readonly cacheTTL?: "5m" | "1h" | (string & {});
|
|
64
|
+
/** Responses API count of stable input items to anchor for caching, sent as top-level `cache_anchor_items`. */
|
|
65
|
+
readonly cacheAnchorItems?: number;
|
|
66
|
+
};
|
|
67
|
+
export interface EvaluationOptions {
|
|
68
|
+
readonly [key: string]: unknown;
|
|
69
|
+
readonly gateway?: GatewayOptions;
|
|
12
70
|
}
|
|
13
|
-
export type Options = ProviderAuthOption<"optional"> & {
|
|
71
|
+
export type Options = Omit<RouteDefaultsInput, "providerOptions"> & ProviderAuthOption<"optional"> & {
|
|
14
72
|
readonly baseURL?: string;
|
|
15
|
-
readonly
|
|
16
|
-
|
|
73
|
+
readonly providerOptions?: ProviderOptionsInput;
|
|
74
|
+
};
|
|
75
|
+
export type Settings = ProviderPackage.Settings & ProviderOptionsInput & {
|
|
76
|
+
readonly apiKey?: string;
|
|
17
77
|
};
|
|
78
|
+
export declare const routes: Route<{
|
|
79
|
+
readonly [x: string]: unknown;
|
|
80
|
+
}, import("../route/transport/http.js").HttpPrepared<string>, import("../route/client.js").CompactionOperations | undefined>[];
|
|
18
81
|
export declare const configure: (input?: Options) => {
|
|
19
82
|
id: string & import("effect/Brand").Brand<"AI.ProviderID">;
|
|
83
|
+
model: (modelID: string | ModelID) => import("../schema/options.js").LanguageModel<ProviderOptionsInput, import("../route/client.js").CompactionOperations | undefined>;
|
|
84
|
+
messages: (modelID: string | ModelID) => import("../schema/options.js").LanguageModel<ProviderOptionsInput, import("../route/client.js").CompactionOperations | undefined>;
|
|
85
|
+
responses: (modelID: string | ModelID) => import("../schema/options.js").LanguageModel<ProviderOptionsInput, import("../route/client.js").CompactionOperations | undefined>;
|
|
86
|
+
chat: (modelID: string | ModelID) => import("../schema/options.js").LanguageModel<ProviderOptionsInput, import("../route/client.js").CompactionOperations | undefined>;
|
|
20
87
|
experimental: {
|
|
21
88
|
evaluation: (modelID: string | ModelID) => EvaluationModel<EvaluationOptions>;
|
|
22
89
|
};
|
|
@@ -24,11 +91,19 @@ export declare const configure: (input?: Options) => {
|
|
|
24
91
|
};
|
|
25
92
|
export declare const provider: {
|
|
26
93
|
id: string & import("effect/Brand").Brand<"AI.ProviderID">;
|
|
94
|
+
model: (modelID: string | ModelID) => import("../schema/options.js").LanguageModel<ProviderOptionsInput, import("../route/client.js").CompactionOperations | undefined>;
|
|
95
|
+
messages: (modelID: string | ModelID) => import("../schema/options.js").LanguageModel<ProviderOptionsInput, import("../route/client.js").CompactionOperations | undefined>;
|
|
96
|
+
responses: (modelID: string | ModelID) => import("../schema/options.js").LanguageModel<ProviderOptionsInput, import("../route/client.js").CompactionOperations | undefined>;
|
|
97
|
+
chat: (modelID: string | ModelID) => import("../schema/options.js").LanguageModel<ProviderOptionsInput, import("../route/client.js").CompactionOperations | undefined>;
|
|
27
98
|
experimental: {
|
|
28
99
|
evaluation: (modelID: string | ModelID) => EvaluationModel<EvaluationOptions>;
|
|
29
100
|
};
|
|
30
101
|
configure: (input?: Options) => {
|
|
31
102
|
id: string & import("effect/Brand").Brand<"AI.ProviderID">;
|
|
103
|
+
model: (modelID: string | ModelID) => import("../schema/options.js").LanguageModel<ProviderOptionsInput, import("../route/client.js").CompactionOperations | undefined>;
|
|
104
|
+
messages: (modelID: string | ModelID) => import("../schema/options.js").LanguageModel<ProviderOptionsInput, import("../route/client.js").CompactionOperations | undefined>;
|
|
105
|
+
responses: (modelID: string | ModelID) => import("../schema/options.js").LanguageModel<ProviderOptionsInput, import("../route/client.js").CompactionOperations | undefined>;
|
|
106
|
+
chat: (modelID: string | ModelID) => import("../schema/options.js").LanguageModel<ProviderOptionsInput, import("../route/client.js").CompactionOperations | undefined>;
|
|
32
107
|
experimental: {
|
|
33
108
|
evaluation: (modelID: string | ModelID) => EvaluationModel<EvaluationOptions>;
|
|
34
109
|
};
|
|
@@ -38,4 +113,11 @@ export declare const provider: {
|
|
|
38
113
|
export declare const experimental: {
|
|
39
114
|
evaluation: (modelID: string | ModelID) => EvaluationModel<EvaluationOptions>;
|
|
40
115
|
};
|
|
116
|
+
export declare const messages: (modelID: string | ModelID) => import("../schema/options.js").LanguageModel<ProviderOptionsInput, import("../route/client.js").CompactionOperations | undefined>;
|
|
117
|
+
export declare const responses: (modelID: string | ModelID) => import("../schema/options.js").LanguageModel<ProviderOptionsInput, import("../route/client.js").CompactionOperations | undefined>;
|
|
118
|
+
export declare const chat: (modelID: string | ModelID) => import("../schema/options.js").LanguageModel<ProviderOptionsInput, import("../route/client.js").CompactionOperations | undefined>;
|
|
119
|
+
export declare const model: ProviderPackage.Definition<Settings, ProviderOptionsInput>["model"];
|
|
120
|
+
export declare const messagesModel: ProviderPackage.Definition<Settings, ProviderOptionsInput>["model"];
|
|
121
|
+
export declare const responsesModel: ProviderPackage.Definition<Settings, ProviderOptionsInput>["model"];
|
|
122
|
+
export declare const chatModel: ProviderPackage.Definition<Settings, ProviderOptionsInput>["model"];
|
|
41
123
|
export * as VercelAIGateway from "./vercel-ai-gateway.js";
|
|
@@ -1,11 +1,111 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
2
|
import { Headers, HttpClientRequest } from "effect/unstable/http";
|
|
3
3
|
import { EvaluationAnswer, EvaluationInput, EvaluationModel, EvaluationQuestion, EvaluationResponse, EvaluationRounding, } from "../experimental/evaluation.js";
|
|
4
|
+
import { AnthropicMessages } from "../protocols/anthropic-messages.js";
|
|
5
|
+
import { OpenAIChat } from "../protocols/openai-chat.js";
|
|
6
|
+
import { OpenResponses } from "../protocols/open-responses.js";
|
|
7
|
+
import { optionalNull, ProviderShared } from "../protocols/shared.js";
|
|
8
|
+
import { gatewayProtocol } from "../protocols/utils/gateway-protocol.js";
|
|
4
9
|
import { Auth } from "../route/auth.js";
|
|
5
10
|
import { AuthOptions } from "../route/auth-options.js";
|
|
6
|
-
import {
|
|
11
|
+
import { Route } from "../route/client.js";
|
|
12
|
+
import { Endpoint } from "../route/endpoint.js";
|
|
13
|
+
import { Framing } from "../route/framing.js";
|
|
14
|
+
import { AIError, HttpContext, HttpOptions, InvalidProviderOutputError, InvalidRequestError, LLMRequest, ModelID, ProviderID, ProviderMetadata, ReasoningEffort, Usage, } from "../schema/index.js";
|
|
7
15
|
export const id = ProviderID.make("vercel-ai-gateway");
|
|
8
16
|
const baseURL = "https://ai-gateway.vercel.sh/v1";
|
|
17
|
+
const GatewayOptionsSchema = Schema.Struct({
|
|
18
|
+
gateway: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
19
|
+
upstream: Schema.optional(Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Unknown))),
|
|
20
|
+
reasoningEffort: Schema.optional(ReasoningEffort),
|
|
21
|
+
thinking: Schema.optional(Schema.Struct({
|
|
22
|
+
type: Schema.String,
|
|
23
|
+
budgetTokens: Schema.optional(Schema.Number),
|
|
24
|
+
budget_tokens: Schema.optional(Schema.Number),
|
|
25
|
+
})),
|
|
26
|
+
cacheTTL: Schema.optional(Schema.String),
|
|
27
|
+
cacheAnchorItems: Schema.optional(Schema.Number),
|
|
28
|
+
});
|
|
29
|
+
const decodeOptions = ProviderShared.validateWith(Schema.decodeUnknownEffect(GatewayOptionsSchema));
|
|
30
|
+
const prepare = (api) => Effect.fnUntraced(function* (request) {
|
|
31
|
+
const options = yield* decodeOptions(request.providerOptions ?? {});
|
|
32
|
+
const providerOptions = (() => {
|
|
33
|
+
if (!options.upstream && !options.gateway)
|
|
34
|
+
return undefined;
|
|
35
|
+
if (!options.gateway)
|
|
36
|
+
return options.upstream;
|
|
37
|
+
return { ...options.upstream, gateway: options.gateway };
|
|
38
|
+
})();
|
|
39
|
+
switch (api) {
|
|
40
|
+
case "messages": {
|
|
41
|
+
const effort = options.reasoningEffort;
|
|
42
|
+
if (effort === undefined)
|
|
43
|
+
return { request, body: { providerOptions } };
|
|
44
|
+
const enabled = effort !== "none";
|
|
45
|
+
const thinking = request.providerOptions?.thinking ?? { type: enabled ? "adaptive" : "disabled" };
|
|
46
|
+
const next = LLMRequest.update(request, {
|
|
47
|
+
providerOptions: {
|
|
48
|
+
...request.providerOptions,
|
|
49
|
+
effort: enabled ? effort : undefined,
|
|
50
|
+
thinking,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
return { request: next, body: { providerOptions } };
|
|
54
|
+
}
|
|
55
|
+
case "responses":
|
|
56
|
+
return {
|
|
57
|
+
request,
|
|
58
|
+
body: {
|
|
59
|
+
providerOptions,
|
|
60
|
+
cache_ttl: options.cacheTTL,
|
|
61
|
+
cache_anchor_items: options.cacheAnchorItems,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
case "chat": {
|
|
65
|
+
if (!options.thinking)
|
|
66
|
+
return { request, body: { providerOptions } };
|
|
67
|
+
const reasoning = {
|
|
68
|
+
enabled: options.thinking.type !== "disabled",
|
|
69
|
+
max_tokens: options.thinking.budgetTokens ?? options.thinking.budget_tokens,
|
|
70
|
+
};
|
|
71
|
+
return { request, body: { providerOptions, reasoning } };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
const route = (input) => Route.make({
|
|
76
|
+
id: input.id,
|
|
77
|
+
provider: id,
|
|
78
|
+
providerMetadataKey: id,
|
|
79
|
+
protocol: gatewayProtocol(input.protocol, { id: input.id, prepare: prepare(input.api) }),
|
|
80
|
+
endpoint: Endpoint.path(input.path, { baseURL }),
|
|
81
|
+
framing: input.framing,
|
|
82
|
+
headers: ({ request }) => request.promptCacheKey ? { "x-session-affinity": request.promptCacheKey } : {},
|
|
83
|
+
defaults: input.defaults,
|
|
84
|
+
});
|
|
85
|
+
const messagesRoute = route({
|
|
86
|
+
id: "vercel-ai-gateway-messages",
|
|
87
|
+
protocol: AnthropicMessages.protocol,
|
|
88
|
+
api: "messages",
|
|
89
|
+
path: "/messages",
|
|
90
|
+
framing: AnthropicMessages.framing,
|
|
91
|
+
defaults: { headers: { "anthropic-version": "2023-06-01" } },
|
|
92
|
+
});
|
|
93
|
+
const responsesRoute = route({
|
|
94
|
+
id: "vercel-ai-gateway-responses",
|
|
95
|
+
protocol: OpenResponses.protocol,
|
|
96
|
+
api: "responses",
|
|
97
|
+
path: "/responses",
|
|
98
|
+
framing: Framing.sse,
|
|
99
|
+
defaults: { providerOptions: { store: false, include: ["reasoning.encrypted_content"] } },
|
|
100
|
+
});
|
|
101
|
+
const chatRoute = route({
|
|
102
|
+
id: "vercel-ai-gateway-chat",
|
|
103
|
+
protocol: OpenAIChat.protocol,
|
|
104
|
+
api: "chat",
|
|
105
|
+
path: "/chat/completions",
|
|
106
|
+
framing: OpenAIChat.framing,
|
|
107
|
+
});
|
|
108
|
+
export const routes = [messagesRoute, responsesRoute, chatRoute];
|
|
9
109
|
const Request = Schema.StructWithRest(Schema.Struct({
|
|
10
110
|
model: Schema.String,
|
|
11
111
|
state: EvaluationInput,
|
|
@@ -13,16 +113,39 @@ const Request = Schema.StructWithRest(Schema.Struct({
|
|
|
13
113
|
providerOptions: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
14
114
|
}), [Schema.Record(Schema.String, Schema.Any)]);
|
|
15
115
|
const Response = Schema.Struct({
|
|
16
|
-
model:
|
|
116
|
+
model: optionalNull(Schema.String),
|
|
17
117
|
answers: Schema.Record(Schema.String, EvaluationAnswer),
|
|
18
|
-
usage:
|
|
19
|
-
inputTokens:
|
|
20
|
-
outputTokens:
|
|
118
|
+
usage: optionalNull(Schema.Struct({
|
|
119
|
+
inputTokens: optionalNull(Schema.Number),
|
|
120
|
+
outputTokens: optionalNull(Schema.Number),
|
|
21
121
|
})),
|
|
22
|
-
rounding:
|
|
23
|
-
providerMetadata:
|
|
122
|
+
rounding: optionalNull(EvaluationRounding),
|
|
123
|
+
providerMetadata: optionalNull(ProviderMetadata),
|
|
24
124
|
});
|
|
25
125
|
export const configure = (input = {}) => {
|
|
126
|
+
const { apiKey: _apiKey, auth: _auth, baseURL: endpoint, ...defaults } = input;
|
|
127
|
+
const configured = {
|
|
128
|
+
...defaults,
|
|
129
|
+
endpoint: { baseURL: endpoint ?? baseURL },
|
|
130
|
+
auth: AuthOptions.bearer(input, ["AI_GATEWAY_API_KEY", "VERCEL_OIDC_TOKEN"]),
|
|
131
|
+
};
|
|
132
|
+
const messages = (modelID) => messagesRoute.with(configured).model({
|
|
133
|
+
id: modelID,
|
|
134
|
+
// Recorded Gateway translations for non-Claude models return thinking with empty signatures.
|
|
135
|
+
compatibility: { requireSignature: modelID.startsWith("anthropic/") },
|
|
136
|
+
});
|
|
137
|
+
const responses = (modelID) => responsesRoute.with(configured).model({ id: modelID });
|
|
138
|
+
const chat = (modelID) => chatRoute
|
|
139
|
+
.with(configured)
|
|
140
|
+
.model({ id: modelID, compatibility: { reasoningField: "reasoning" } });
|
|
141
|
+
// Each family uses the API whose Gateway translation carries its reasoning state across turns.
|
|
142
|
+
const model = (modelID) => {
|
|
143
|
+
if (/^(openai\/gpt-|spacexai\/grok-)/.test(modelID))
|
|
144
|
+
return responses(modelID);
|
|
145
|
+
if (modelID.startsWith("meta/muse-"))
|
|
146
|
+
return chat(modelID);
|
|
147
|
+
return messages(modelID);
|
|
148
|
+
};
|
|
26
149
|
const evaluation = (modelID) => EvaluationModel.make({
|
|
27
150
|
id: modelID,
|
|
28
151
|
provider: id,
|
|
@@ -39,7 +162,7 @@ export const configure = (input = {}) => {
|
|
|
39
162
|
questions: req.questions,
|
|
40
163
|
providerOptions: req.options,
|
|
41
164
|
}).pipe(Effect.mapError((cause) => new AIError({ reason: new InvalidRequestError({ message: cause.message, cause }) })));
|
|
42
|
-
const headers = yield* Auth.toEffect(
|
|
165
|
+
const headers = yield* Auth.toEffect(configured.auth)({
|
|
43
166
|
request: req,
|
|
44
167
|
method: "POST",
|
|
45
168
|
url: url.toString(),
|
|
@@ -59,27 +182,44 @@ export const configure = (input = {}) => {
|
|
|
59
182
|
});
|
|
60
183
|
const text = yield* res.text.pipe(Effect.mapError((cause) => fail("Failed to read the Vercel AI Gateway evaluation response", cause)));
|
|
61
184
|
const data = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Response))(text).pipe(Effect.mapError((cause) => fail("Vercel AI Gateway returned an invalid evaluation response", cause, text)));
|
|
185
|
+
const inputTokens = data.usage?.inputTokens ?? undefined;
|
|
186
|
+
const outputTokens = data.usage?.outputTokens ?? undefined;
|
|
187
|
+
const usage = data.usage
|
|
188
|
+
? new Usage({
|
|
189
|
+
inputTokens,
|
|
190
|
+
outputTokens,
|
|
191
|
+
totalTokens: ProviderShared.totalTokens(inputTokens, outputTokens, undefined),
|
|
192
|
+
providerMetadata: { gateway: data.usage },
|
|
193
|
+
})
|
|
194
|
+
: undefined;
|
|
62
195
|
return new EvaluationResponse({
|
|
63
196
|
model: ModelID.make(data.model ?? req.model.id),
|
|
64
197
|
answers: data.answers,
|
|
65
|
-
usage
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
outputTokens: data.usage.outputTokens,
|
|
69
|
-
totalTokens: data.usage.inputTokens === undefined && data.usage.outputTokens === undefined
|
|
70
|
-
? undefined
|
|
71
|
-
: (data.usage.inputTokens ?? 0) + (data.usage.outputTokens ?? 0),
|
|
72
|
-
providerMetadata: { gateway: data.usage },
|
|
73
|
-
})
|
|
74
|
-
: undefined,
|
|
75
|
-
rounding: data.rounding,
|
|
76
|
-
providerMetadata: data.providerMetadata,
|
|
198
|
+
usage,
|
|
199
|
+
rounding: data.rounding ?? undefined,
|
|
200
|
+
providerMetadata: data.providerMetadata ?? undefined,
|
|
77
201
|
});
|
|
78
202
|
}),
|
|
79
203
|
},
|
|
80
204
|
});
|
|
81
|
-
return { id, experimental: { evaluation }, configure };
|
|
205
|
+
return { id, model, messages, responses, chat, experimental: { evaluation }, configure };
|
|
82
206
|
};
|
|
83
207
|
export const provider = configure();
|
|
84
208
|
export const experimental = provider.experimental;
|
|
209
|
+
export const messages = provider.messages;
|
|
210
|
+
export const responses = provider.responses;
|
|
211
|
+
export const chat = provider.chat;
|
|
212
|
+
export const model = (modelID, settings) => fromSettings(settings).model(modelID);
|
|
213
|
+
export const messagesModel = (modelID, settings) => fromSettings(settings).messages(modelID);
|
|
214
|
+
export const responsesModel = (modelID, settings) => fromSettings(settings).responses(modelID);
|
|
215
|
+
export const chatModel = (modelID, settings) => fromSettings(settings).chat(modelID);
|
|
216
|
+
function fromSettings({ apiKey, baseURL, headers, body, ...providerOptions }) {
|
|
217
|
+
return configure({
|
|
218
|
+
apiKey,
|
|
219
|
+
baseURL,
|
|
220
|
+
headers,
|
|
221
|
+
http: body === undefined ? undefined : { body: { ...body } },
|
|
222
|
+
providerOptions,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
85
225
|
export * as VercelAIGateway from "./vercel-ai-gateway.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
|
-
"version": "0.0.0-dev-
|
|
3
|
+
"version": "0.0.0-dev-20592",
|
|
4
4
|
"name": "@opencode/ai",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@clack/prompts": "1.0.0-alpha.1",
|
|
36
36
|
"@effect/platform-node": "4.0.0-rc.112",
|
|
37
|
-
"@opencode/http-recorder": "0.0.0-dev-
|
|
37
|
+
"@opencode/http-recorder": "0.0.0-dev-20592",
|
|
38
38
|
"@tsconfig/bun": "1.0.9",
|
|
39
39
|
"@types/bun": "1.4.0",
|
|
40
40
|
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"@aws-sdk/credential-providers": "3.1057.0",
|
|
45
45
|
"@smithy/eventstream-codec": "4.2.14",
|
|
46
46
|
"@smithy/util-utf8": "4.2.2",
|
|
47
|
-
"@opencode/schema": "0.0.0-dev-
|
|
47
|
+
"@opencode/schema": "0.0.0-dev-20592",
|
|
48
48
|
"aws4fetch": "1.0.20",
|
|
49
49
|
"effect": "4.0.0-rc.112",
|
|
50
50
|
"google-auth-library": "10.5.0"
|