@opencode/ai 2.0.27 → 2.0.29
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 +20 -11
- package/dist/media.d.ts +1 -1
- package/dist/protocols/alibaba-chat.d.ts +2 -2
- package/dist/protocols/alibaba-messages.d.ts +1 -1
- package/dist/protocols/gemini.d.ts +73 -9
- package/dist/protocols/gemini.js +111 -17
- package/dist/protocols/mistral-chat.js +3 -1
- package/dist/protocols/open-responses-channel.js +6 -1
- package/dist/protocols/openai-chat.js +6 -2
- package/dist/protocols/shared.d.ts +5 -1
- package/dist/protocols/shared.js +28 -2
- package/dist/protocols/utils/responses-compaction.js +6 -1
- package/dist/providers/alibaba.d.ts +5 -5
- package/dist/providers/google-vertex.d.ts +2 -1
- package/dist/providers/google-vertex.js +1 -2
- package/dist/providers/google.d.ts +2 -1
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.js +1 -1
- package/dist/providers/{opencode-zen.d.ts → opencode.d.ts} +1 -1
- package/dist/providers/{opencode-zen.js → opencode.js} +2 -2
- package/dist/route/transport/http.js +4 -3
- package/dist/schema/options.d.ts +15 -1
- package/dist/schema/options.js +25 -5
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -182,7 +182,7 @@ console.log(response.answers.department.choice)
|
|
|
182
182
|
console.log(response.answers.refund.probability)
|
|
183
183
|
```
|
|
184
184
|
|
|
185
|
-
`TypeSafeAI` reads `TYPESAFE_API_KEY`. `
|
|
185
|
+
`TypeSafeAI` reads `TYPESAFE_API_KEY`. `OpenCode` exposes the same selector and reads
|
|
186
186
|
`OPENCODE_API_KEY`. OpenRouter and Vercel AI Gateway use the same provider shape:
|
|
187
187
|
|
|
188
188
|
```ts
|
package/dist/cache-policy.js
CHANGED
|
@@ -143,17 +143,7 @@ const countHints = (request) => countToolHints(request.tools) +
|
|
|
143
143
|
request.messages.reduce((count, message) => count +
|
|
144
144
|
message.content.reduce((contentCount, part) => contentCount + ("cache" in part && part.cache !== undefined ? 1 : 0), 0), 0);
|
|
145
145
|
export const applyCachePolicy = (request) => {
|
|
146
|
-
const
|
|
147
|
-
if (!RESPECTS_INLINE_HINTS.has(route))
|
|
148
|
-
return request;
|
|
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")
|
|
153
|
-
? request.model.id.toLowerCase().startsWith("qwen")
|
|
154
|
-
? QWEN
|
|
155
|
-
: NONE
|
|
156
|
-
: resolve(request.cache);
|
|
146
|
+
const policy = selectPolicy(request);
|
|
157
147
|
if (!policy.tools && !policy.system && !policy.messages)
|
|
158
148
|
return request;
|
|
159
149
|
const hint = makeHint(policy.ttlSeconds);
|
|
@@ -165,3 +155,22 @@ export const applyCachePolicy = (request) => {
|
|
|
165
155
|
return request;
|
|
166
156
|
return LLMRequest.update(request, { tools, system, messages });
|
|
167
157
|
};
|
|
158
|
+
// A declared cache format overrides the route allowlist and its model-ID heuristics.
|
|
159
|
+
const selectPolicy = (request) => {
|
|
160
|
+
const format = request.model.compatibility?.cacheFormat;
|
|
161
|
+
const route = request.model.route.id;
|
|
162
|
+
const auto = request.cache === undefined || request.cache === "auto";
|
|
163
|
+
if (format === "alibaba")
|
|
164
|
+
return auto ? QWEN : { ...resolve(request.cache), tools: false };
|
|
165
|
+
if (format === "anthropic")
|
|
166
|
+
return resolve(request.cache);
|
|
167
|
+
if (!RESPECTS_INLINE_HINTS.has(route))
|
|
168
|
+
return NONE;
|
|
169
|
+
if (!auto)
|
|
170
|
+
return resolve(request.cache);
|
|
171
|
+
if (route === "openrouter" || route === "vercel-ai-gateway-messages")
|
|
172
|
+
return gatewayPolicy(request.model.id);
|
|
173
|
+
if (route === "alibaba-chat")
|
|
174
|
+
return request.model.id.toLowerCase().startsWith("qwen") ? QWEN : NONE;
|
|
175
|
+
return AUTO;
|
|
176
|
+
};
|
package/dist/media.d.ts
CHANGED
|
@@ -100,12 +100,12 @@ export declare class Asset {
|
|
|
100
100
|
};
|
|
101
101
|
info: {
|
|
102
102
|
readonly format?: string | undefined;
|
|
103
|
+
readonly encoding?: string | undefined;
|
|
103
104
|
readonly width?: number | undefined;
|
|
104
105
|
readonly height?: number | undefined;
|
|
105
106
|
readonly durationSeconds?: number | undefined;
|
|
106
107
|
readonly sampleRate?: number | undefined;
|
|
107
108
|
readonly channels?: number | undefined;
|
|
108
|
-
readonly encoding?: string | undefined;
|
|
109
109
|
} | undefined;
|
|
110
110
|
providerMetadata: {
|
|
111
111
|
readonly [x: string]: {
|
|
@@ -10,7 +10,7 @@ declare const Options: Schema.Struct<{
|
|
|
10
10
|
readonly preserveThinking: Schema.optional<Schema.Boolean>;
|
|
11
11
|
readonly clearThinking: Schema.optional<Schema.Boolean>;
|
|
12
12
|
readonly thinking: Schema.optional<Schema.Struct<{
|
|
13
|
-
readonly type: Schema.declare<
|
|
13
|
+
readonly type: Schema.declare<(string & {}) | "disabled" | "adaptive", (string & {}) | "disabled" | "adaptive">;
|
|
14
14
|
}>>;
|
|
15
15
|
readonly toolStream: Schema.optional<Schema.Boolean>;
|
|
16
16
|
readonly parallelToolCalls: Schema.optional<Schema.Boolean>;
|
|
@@ -140,7 +140,7 @@ export declare const protocol: Protocol<{
|
|
|
140
140
|
readonly temperature?: number | undefined;
|
|
141
141
|
readonly seed?: number | undefined;
|
|
142
142
|
readonly thinking?: {
|
|
143
|
-
readonly type:
|
|
143
|
+
readonly type: (string & {}) | "disabled" | "adaptive";
|
|
144
144
|
} | undefined;
|
|
145
145
|
readonly tool_choice?: "required" | "auto" | "none" | {
|
|
146
146
|
readonly type: "function";
|
|
@@ -5,7 +5,7 @@ import { OpenResponsesOptions } from "./utils/open-responses-options.js";
|
|
|
5
5
|
declare const Options: Schema.Struct<{
|
|
6
6
|
readonly effort: Schema.optional<Schema.declare<OpenResponsesOptions.ReasoningEffort, OpenResponsesOptions.ReasoningEffort>>;
|
|
7
7
|
readonly thinking: Schema.optional<Schema.Struct<{
|
|
8
|
-
readonly type: Schema.declare<"enabled" | "disabled"
|
|
8
|
+
readonly type: Schema.declare<(string & {}) | "enabled" | "disabled", (string & {}) | "enabled" | "disabled">;
|
|
9
9
|
readonly budgetTokens: Schema.optional<Schema.Int>;
|
|
10
10
|
readonly budget_tokens: Schema.optional<Schema.Int>;
|
|
11
11
|
}>>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
2
|
import { Route } from "../route/client.js";
|
|
3
3
|
import { Protocol } from "../route/protocol.js";
|
|
4
|
+
import { HttpTransport, type Transport, type TransportPrepareInput } from "../route/transport/index.js";
|
|
4
5
|
import { type ProviderOptions } from "../schema/index.js";
|
|
5
6
|
import { Lifecycle } from "./utils/lifecycle.js";
|
|
6
7
|
export declare const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
|
@@ -100,8 +101,54 @@ declare const GeminiBody: Schema.Struct<{
|
|
|
100
101
|
readonly thinkingLevel: Schema.optional<Schema.declare<import("../schema/options.js").OpenString<"minimal" | "low" | "medium" | "high">, import("../schema/options.js").OpenString<"minimal" | "low" | "medium" | "high">>>;
|
|
101
102
|
}>>;
|
|
102
103
|
}>>;
|
|
104
|
+
continuationToken: Schema.optional<Schema.String>;
|
|
103
105
|
}>;
|
|
104
106
|
export type GeminiBody = Schema.Schema.Type<typeof GeminiBody>;
|
|
107
|
+
declare const GeminiUsage: Schema.Struct<{
|
|
108
|
+
readonly cachedContentTokenCount: Schema.optional<Schema.NullOr<Schema.Number>>;
|
|
109
|
+
readonly thoughtsTokenCount: Schema.optional<Schema.NullOr<Schema.Number>>;
|
|
110
|
+
readonly promptTokenCount: Schema.optional<Schema.NullOr<Schema.Number>>;
|
|
111
|
+
readonly candidatesTokenCount: Schema.optional<Schema.NullOr<Schema.Number>>;
|
|
112
|
+
readonly totalTokenCount: Schema.optional<Schema.NullOr<Schema.Number>>;
|
|
113
|
+
}>;
|
|
114
|
+
type GeminiUsage = Schema.Schema.Type<typeof GeminiUsage>;
|
|
115
|
+
declare const GeminiPromptFeedback: Schema.StructWithRest<Schema.Struct<{
|
|
116
|
+
readonly blockReason: Schema.optional<Schema.NullOr<Schema.String>>;
|
|
117
|
+
readonly blockReasonMessage: Schema.optional<Schema.NullOr<Schema.String>>;
|
|
118
|
+
readonly safetyRatings: Schema.optional<Schema.NullOr<Schema.Unknown>>;
|
|
119
|
+
}>, readonly [Schema.$Record<Schema.String, Schema.Unknown>]>;
|
|
120
|
+
type GeminiPromptFeedback = Schema.Schema.Type<typeof GeminiPromptFeedback>;
|
|
121
|
+
interface ParserState {
|
|
122
|
+
readonly route: string;
|
|
123
|
+
readonly providerMetadataKey: string;
|
|
124
|
+
readonly finishReason?: string;
|
|
125
|
+
readonly hasToolCalls: boolean;
|
|
126
|
+
readonly promptFeedback?: GeminiPromptFeedback;
|
|
127
|
+
readonly usage?: GeminiUsage;
|
|
128
|
+
/** Usage of the earlier requests of a response Gemini continued across requests. */
|
|
129
|
+
readonly continuedUsage?: ReadonlyArray<GeminiUsage>;
|
|
130
|
+
readonly lifecycle: Lifecycle.State;
|
|
131
|
+
readonly reasoningSignature?: string;
|
|
132
|
+
readonly textSignature?: string;
|
|
133
|
+
readonly reasoningId?: string;
|
|
134
|
+
readonly textId?: string;
|
|
135
|
+
readonly nextReasoningId: number;
|
|
136
|
+
readonly nextTextId: number;
|
|
137
|
+
readonly seenCallIds?: ReadonlySet<string>;
|
|
138
|
+
}
|
|
139
|
+
/** Marks where the transport starts another request for the same response; provider JSON can never match it. */
|
|
140
|
+
declare class NextRequest {
|
|
141
|
+
}
|
|
142
|
+
export interface Prepared {
|
|
143
|
+
readonly http: HttpTransport.HttpPrepared<string>;
|
|
144
|
+
readonly input: TransportPrepareInput<GeminiBody>;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* SSE transport that continues responses Gemini splits across requests. Chunks carry `continuationToken` checkpoints,
|
|
148
|
+
* and a request that reaches its output cap finishes with `CONTINUATION`. The transport resends the original body with
|
|
149
|
+
* the latest token, which also resumes a request that ended or dropped before finishing.
|
|
150
|
+
*/
|
|
151
|
+
export declare const transport: Transport<GeminiBody, Prepared, unknown>;
|
|
105
152
|
/**
|
|
106
153
|
* The Gemini protocol — request body construction, body schema, and the
|
|
107
154
|
* streaming-event state machine shared by Google AI Studio and Vertex Gemini.
|
|
@@ -190,7 +237,8 @@ export declare const protocol: Protocol<{
|
|
|
190
237
|
} | undefined;
|
|
191
238
|
readonly maxOutputTokens?: number | undefined;
|
|
192
239
|
} | undefined;
|
|
193
|
-
|
|
240
|
+
readonly continuationToken?: string | undefined;
|
|
241
|
+
}, NextRequest | {
|
|
194
242
|
readonly error?: unknown;
|
|
195
243
|
readonly candidates?: readonly {
|
|
196
244
|
readonly content?: {
|
|
@@ -213,13 +261,28 @@ export declare const protocol: Protocol<{
|
|
|
213
261
|
readonly thoughtsTokenCount?: number | null | undefined;
|
|
214
262
|
} | null | undefined;
|
|
215
263
|
}, {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
264
|
+
readonly error?: unknown;
|
|
265
|
+
readonly candidates?: readonly {
|
|
266
|
+
readonly content?: {
|
|
267
|
+
readonly parts?: readonly unknown[] | null | undefined;
|
|
268
|
+
readonly role?: "model" | "user" | null | undefined;
|
|
269
|
+
} | null | undefined;
|
|
270
|
+
readonly finishReason?: string | null | undefined;
|
|
271
|
+
}[] | null | undefined;
|
|
272
|
+
readonly promptFeedback?: {
|
|
273
|
+
readonly [x: string]: unknown;
|
|
274
|
+
readonly blockReason?: string | null | undefined;
|
|
275
|
+
readonly blockReasonMessage?: string | null | undefined;
|
|
276
|
+
readonly safetyRatings?: unknown;
|
|
277
|
+
} | null | undefined;
|
|
278
|
+
readonly usageMetadata?: {
|
|
279
|
+
readonly promptTokenCount?: number | null | undefined;
|
|
280
|
+
readonly candidatesTokenCount?: number | null | undefined;
|
|
281
|
+
readonly totalTokenCount?: number | null | undefined;
|
|
282
|
+
readonly cachedContentTokenCount?: number | null | undefined;
|
|
283
|
+
readonly thoughtsTokenCount?: number | null | undefined;
|
|
284
|
+
} | null | undefined;
|
|
285
|
+
} | NextRequest, ParserState>;
|
|
223
286
|
export declare const route: Route<{
|
|
224
287
|
readonly contents: readonly {
|
|
225
288
|
readonly parts?: readonly ({
|
|
@@ -304,5 +367,6 @@ export declare const route: Route<{
|
|
|
304
367
|
} | undefined;
|
|
305
368
|
readonly maxOutputTokens?: number | undefined;
|
|
306
369
|
} | undefined;
|
|
307
|
-
|
|
370
|
+
readonly continuationToken?: string | undefined;
|
|
371
|
+
}, Prepared, import("../route/client.js").CompactionOperations | undefined>;
|
|
308
372
|
export * as Gemini from "./gemini.js";
|
package/dist/protocols/gemini.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { Effect, Option, Schema } from "effect";
|
|
1
|
+
import { Effect, Option, Schema, Stream } from "effect";
|
|
2
2
|
import { Tool } from "@opencode/schema/tool";
|
|
3
3
|
import { Route } from "../route/client.js";
|
|
4
4
|
import { Auth } from "../route/auth.js";
|
|
5
5
|
import { Endpoint } from "../route/endpoint.js";
|
|
6
|
-
import { Framing } from "../route/framing.js";
|
|
7
6
|
import { Protocol } from "../route/protocol.js";
|
|
7
|
+
import { HttpTransport, } from "../route/transport/index.js";
|
|
8
8
|
import { AIError, LLMEvent, Usage, } from "../schema/index.js";
|
|
9
9
|
import { classifyProviderFailure } from "../provider-error.js";
|
|
10
10
|
import { Media } from "../media.js";
|
|
@@ -12,6 +12,7 @@ import { JsonObject, knownString, lenient, optionalArray, optionalNull, Provider
|
|
|
12
12
|
import { GeminiGenerateContent } from "./utils/gemini-generate-content.js";
|
|
13
13
|
import { Lifecycle } from "./utils/lifecycle.js";
|
|
14
14
|
const ADAPTER = "gemini";
|
|
15
|
+
const CONTINUATION = "CONTINUATION";
|
|
15
16
|
// Google documents this sentinel for replaying Gemini 3 function calls after their original signature was lost.
|
|
16
17
|
const SKIP_THOUGHT_SIGNATURE_VALIDATOR = "skip_thought_signature_validator";
|
|
17
18
|
// Gemini 2.5 rejects a budget under the model's minimum: 512 on Flash-Lite, the highest, and 128 on Pro.
|
|
@@ -162,6 +163,7 @@ const GeminiBodyFields = {
|
|
|
162
163
|
tools: optionalArray(GeminiTool),
|
|
163
164
|
toolConfig: Schema.optional(GeminiToolConfig),
|
|
164
165
|
generationConfig: Schema.optional(GeminiGenerationConfig),
|
|
166
|
+
continuationToken: Schema.optional(Schema.String),
|
|
165
167
|
};
|
|
166
168
|
const GeminiBody = Schema.Struct(GeminiBodyFields);
|
|
167
169
|
const GeminiUsage = Schema.Struct({
|
|
@@ -403,7 +405,7 @@ const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request) {
|
|
|
403
405
|
// `cachedContentTokenCount` subset. `candidatesTokenCount` is *exclusive*
|
|
404
406
|
// of `thoughtsTokenCount` — visible-only, not a total — so we sum the two
|
|
405
407
|
// to produce the inclusive `outputTokens` the rest of the contract expects.
|
|
406
|
-
const mapUsage = (usage, metadataKey) => {
|
|
408
|
+
const mapUsage = (usage, metadataKey, metadata = usage ?? {}) => {
|
|
407
409
|
if (!usage)
|
|
408
410
|
return undefined;
|
|
409
411
|
// Explicit provider nulls decode as `null`; normalize to `undefined` so the
|
|
@@ -425,7 +427,26 @@ const mapUsage = (usage, metadataKey) => {
|
|
|
425
427
|
cacheReadInputTokens: cached,
|
|
426
428
|
reasoningTokens: thoughts,
|
|
427
429
|
totalTokens: ProviderShared.totalTokens(promptTokens, outputTokens, usage.totalTokenCount ?? undefined),
|
|
428
|
-
providerMetadata: providerMetadata(metadataKey,
|
|
430
|
+
providerMetadata: providerMetadata(metadataKey, metadata),
|
|
431
|
+
});
|
|
432
|
+
};
|
|
433
|
+
// Each continued request re-bills the output so far as input. Reporting the first request's input with all output keeps
|
|
434
|
+
// input plus output equal to the response's final context size, under-reporting billed input; the summed billed counts
|
|
435
|
+
// remain in provider metadata.
|
|
436
|
+
const mapContinuedUsage = (requests, metadataKey) => {
|
|
437
|
+
const sum = (field) => ProviderShared.sumTokens(...requests.map((usage) => usage[field] ?? undefined));
|
|
438
|
+
return mapUsage({
|
|
439
|
+
promptTokenCount: requests[0].promptTokenCount,
|
|
440
|
+
cachedContentTokenCount: requests[0].cachedContentTokenCount,
|
|
441
|
+
candidatesTokenCount: sum("candidatesTokenCount"),
|
|
442
|
+
thoughtsTokenCount: sum("thoughtsTokenCount"),
|
|
443
|
+
}, metadataKey, {
|
|
444
|
+
promptTokenCount: sum("promptTokenCount"),
|
|
445
|
+
cachedContentTokenCount: sum("cachedContentTokenCount"),
|
|
446
|
+
candidatesTokenCount: sum("candidatesTokenCount"),
|
|
447
|
+
thoughtsTokenCount: sum("thoughtsTokenCount"),
|
|
448
|
+
totalTokenCount: sum("totalTokenCount"),
|
|
449
|
+
requests: requests.length,
|
|
429
450
|
});
|
|
430
451
|
};
|
|
431
452
|
const mapFinishReason = (finishReason, hasToolCalls) => {
|
|
@@ -451,7 +472,7 @@ const finish = (state) => {
|
|
|
451
472
|
// the "nothing to finish" check below keeps its meaning.
|
|
452
473
|
const promptBlockReason = state.finishReason === undefined ? (state.promptFeedback?.blockReason ?? undefined) : undefined;
|
|
453
474
|
const finishReason = state.finishReason ?? promptBlockReason;
|
|
454
|
-
if (finishReason === undefined && state.usage === undefined)
|
|
475
|
+
if (finishReason === undefined && state.usage === undefined && state.continuedUsage === undefined)
|
|
455
476
|
return [];
|
|
456
477
|
const events = [];
|
|
457
478
|
let lifecycle = state.lifecycle;
|
|
@@ -468,7 +489,9 @@ const finish = (state) => {
|
|
|
468
489
|
normalized: promptBlockReason === undefined ? mapFinishReason(finishReason, state.hasToolCalls) : "content-filter",
|
|
469
490
|
raw: finishReason,
|
|
470
491
|
},
|
|
471
|
-
usage: state.
|
|
492
|
+
usage: state.continuedUsage === undefined
|
|
493
|
+
? mapUsage(state.usage, state.providerMetadataKey)
|
|
494
|
+
: mapContinuedUsage([...state.continuedUsage, state.usage ?? {}], state.providerMetadataKey),
|
|
472
495
|
providerMetadata: state.promptFeedback === undefined
|
|
473
496
|
? undefined
|
|
474
497
|
: providerMetadata(state.providerMetadataKey, { promptFeedback: state.promptFeedback }),
|
|
@@ -476,6 +499,18 @@ const finish = (state) => {
|
|
|
476
499
|
return events;
|
|
477
500
|
};
|
|
478
501
|
const step = (state, event) => {
|
|
502
|
+
// Usage is cumulative within one request, so the next request of the same response counts from zero.
|
|
503
|
+
if (event instanceof NextRequest)
|
|
504
|
+
return Effect.succeed([
|
|
505
|
+
{
|
|
506
|
+
...state,
|
|
507
|
+
finishReason: undefined,
|
|
508
|
+
promptFeedback: undefined,
|
|
509
|
+
usage: undefined,
|
|
510
|
+
continuedUsage: [...(state.continuedUsage ?? []), state.usage ?? {}],
|
|
511
|
+
},
|
|
512
|
+
[],
|
|
513
|
+
]);
|
|
479
514
|
if (ProviderShared.isRecord(event.error)) {
|
|
480
515
|
const body = ProviderShared.encodeJson(event);
|
|
481
516
|
return Effect.fail(new AIError({
|
|
@@ -492,21 +527,17 @@ const step = (state, event) => {
|
|
|
492
527
|
}
|
|
493
528
|
if ("error" in event)
|
|
494
529
|
return Effect.fail(ProviderShared.eventError(state.route, `Invalid ${state.route} stream event`, ProviderShared.encodeJson(event)));
|
|
530
|
+
const candidate = event.candidates?.[0];
|
|
495
531
|
const nextState = {
|
|
496
532
|
...state,
|
|
497
533
|
promptFeedback: event.promptFeedback ?? state.promptFeedback,
|
|
498
|
-
usage: event.usageMetadata
|
|
499
|
-
|
|
500
|
-
: state.usage,
|
|
534
|
+
usage: event.usageMetadata ?? state.usage,
|
|
535
|
+
finishReason: candidate?.finishReason ?? state.finishReason,
|
|
501
536
|
};
|
|
502
|
-
const candidate = event.candidates?.[0];
|
|
503
537
|
if (candidate?.finishReason && mapFinishReason(candidate.finishReason, state.hasToolCalls) === "error")
|
|
504
538
|
return Effect.fail(ProviderShared.eventError(state.route, `Gemini stopped with ${candidate.finishReason}`, ProviderShared.encodeJson(event)));
|
|
505
539
|
if (!candidate?.content)
|
|
506
|
-
return Effect.succeed([
|
|
507
|
-
{ ...nextState, finishReason: candidate?.finishReason ?? nextState.finishReason },
|
|
508
|
-
[],
|
|
509
|
-
]);
|
|
540
|
+
return Effect.succeed([nextState, []]);
|
|
510
541
|
const events = [];
|
|
511
542
|
let hasToolCalls = nextState.hasToolCalls;
|
|
512
543
|
let lifecycle = nextState.lifecycle;
|
|
@@ -625,12 +656,75 @@ const step = (state, event) => {
|
|
|
625
656
|
nextReasoningId,
|
|
626
657
|
nextTextId,
|
|
627
658
|
seenCallIds,
|
|
628
|
-
finishReason: candidate.finishReason ?? nextState.finishReason,
|
|
629
659
|
},
|
|
630
660
|
events,
|
|
631
661
|
]);
|
|
632
662
|
};
|
|
633
663
|
// =============================================================================
|
|
664
|
+
// Transport
|
|
665
|
+
// =============================================================================
|
|
666
|
+
const GeminiCheckpoint = Schema.Struct({
|
|
667
|
+
candidates: optionalNull(Schema.Array(Schema.Struct({
|
|
668
|
+
finishReason: optionalNull(Schema.String),
|
|
669
|
+
continuationToken: optionalNull(Schema.String),
|
|
670
|
+
}))),
|
|
671
|
+
});
|
|
672
|
+
const decodeCheckpoint = Schema.decodeUnknownOption(GeminiCheckpoint);
|
|
673
|
+
/** Marks where the transport starts another request for the same response; provider JSON can never match it. */
|
|
674
|
+
class NextRequest {
|
|
675
|
+
}
|
|
676
|
+
const sse = HttpTransport.sseJson.with();
|
|
677
|
+
/**
|
|
678
|
+
* SSE transport that continues responses Gemini splits across requests. Chunks carry `continuationToken` checkpoints,
|
|
679
|
+
* and a request that reaches its output cap finishes with `CONTINUATION`. The transport resends the original body with
|
|
680
|
+
* the latest token, which also resumes a request that ended or dropped before finishing.
|
|
681
|
+
*/
|
|
682
|
+
export const transport = {
|
|
683
|
+
id: sse.id,
|
|
684
|
+
prepare: (input) => sse.prepare(input).pipe(Effect.map((http) => ({ http, input }))),
|
|
685
|
+
execute: (prepared, request, runtime) => Effect.gen(function* () {
|
|
686
|
+
const route = request.model.route.id;
|
|
687
|
+
const label = `${request.model.provider}/${route}`;
|
|
688
|
+
// Frames are parsed once here, so errors recover each frame's raw text for their body.
|
|
689
|
+
const raw = new WeakMap();
|
|
690
|
+
const respond = (execution) => Stream.suspend(() => {
|
|
691
|
+
let token;
|
|
692
|
+
let finishReason;
|
|
693
|
+
let dropped;
|
|
694
|
+
return execution.frames.pipe(Stream.mapEffect((frame) => ProviderShared.parseJson(route, frame, `Invalid ${label} stream event`).pipe(Effect.map((event) => {
|
|
695
|
+
if (typeof event === "object" && event !== null)
|
|
696
|
+
raw.set(event, frame);
|
|
697
|
+
const candidate = Option.getOrUndefined(decodeCheckpoint(event))?.candidates?.[0];
|
|
698
|
+
token = candidate?.continuationToken ?? (candidate?.finishReason ? undefined : token);
|
|
699
|
+
finishReason = candidate?.finishReason ?? finishReason;
|
|
700
|
+
return event;
|
|
701
|
+
}))), Stream.catchIf(
|
|
702
|
+
// A connection failure may resume; a timeout keeps its meaning and ends the response.
|
|
703
|
+
(error) => error.reason._tag === "Transport" && error.reason.code !== "Timeout", (error) => {
|
|
704
|
+
dropped = error;
|
|
705
|
+
return Stream.empty;
|
|
706
|
+
}), Stream.concat(Stream.suspend(() => {
|
|
707
|
+
if (token !== undefined && (finishReason === undefined || finishReason === CONTINUATION))
|
|
708
|
+
return Stream.concat(Stream.succeed(new NextRequest()), next(token));
|
|
709
|
+
if (dropped)
|
|
710
|
+
return Stream.fail(dropped);
|
|
711
|
+
if (finishReason === CONTINUATION)
|
|
712
|
+
return Stream.fail(ProviderShared.eventError(label, "Gemini continued the response without a token"));
|
|
713
|
+
return Stream.empty;
|
|
714
|
+
})));
|
|
715
|
+
});
|
|
716
|
+
const next = (continuationToken) => Stream.unwrap(sse
|
|
717
|
+
.prepare({ ...prepared.input, body: { ...prepared.input.body, continuationToken } })
|
|
718
|
+
.pipe(Effect.flatMap((continued) => sse.execute(continued, request, runtime)), Effect.map(respond)));
|
|
719
|
+
const first = yield* sse.execute(prepared.http, request, runtime);
|
|
720
|
+
return {
|
|
721
|
+
frames: respond(first),
|
|
722
|
+
http: first.http,
|
|
723
|
+
body: (frame) => (typeof frame === "object" && frame !== null ? raw.get(frame) : undefined),
|
|
724
|
+
};
|
|
725
|
+
}),
|
|
726
|
+
};
|
|
727
|
+
// =============================================================================
|
|
634
728
|
// Protocol And Gemini Route
|
|
635
729
|
// =============================================================================
|
|
636
730
|
/**
|
|
@@ -646,7 +740,7 @@ export const protocol = Protocol.make({
|
|
|
646
740
|
// Gemini's schema rules are this API's default, including for tuned endpoints whose IDs do not name Gemini.
|
|
647
741
|
sanitizer: "gemini",
|
|
648
742
|
stream: {
|
|
649
|
-
event:
|
|
743
|
+
event: Schema.Union([Schema.instanceOf(NextRequest), GeminiEvent]),
|
|
650
744
|
initial: (request) => ({
|
|
651
745
|
route: `${request.model.provider}/${request.model.route.id}`,
|
|
652
746
|
providerMetadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
|
|
@@ -669,6 +763,6 @@ export const route = Route.make({
|
|
|
669
763
|
baseURL: DEFAULT_BASE_URL,
|
|
670
764
|
}),
|
|
671
765
|
auth: Auth.none,
|
|
672
|
-
|
|
766
|
+
transport,
|
|
673
767
|
});
|
|
674
768
|
export * as Gemini from "./gemini.js";
|
|
@@ -317,7 +317,9 @@ export const fromRequest = Effect.fn("MistralChat.fromRequest")(function* (reque
|
|
|
317
317
|
frequency_penalty: request.generation?.frequencyPenalty,
|
|
318
318
|
presence_penalty: request.generation?.presencePenalty,
|
|
319
319
|
stop: request.generation?.stop,
|
|
320
|
-
prompt_cache_key:
|
|
320
|
+
prompt_cache_key: ProviderShared.sendsPromptCacheKey(request)
|
|
321
|
+
? (options.promptCacheKey ?? request.promptCacheKey)
|
|
322
|
+
: undefined,
|
|
321
323
|
safe_prompt: options.safePrompt,
|
|
322
324
|
document_image_limit: options.documentImageLimit,
|
|
323
325
|
document_page_limit: options.documentPageLimit,
|
|
@@ -106,7 +106,12 @@ export const transport = (options) => {
|
|
|
106
106
|
});
|
|
107
107
|
return {
|
|
108
108
|
http: {
|
|
109
|
-
request: ProviderShared.jsonPost({
|
|
109
|
+
request: ProviderShared.jsonPost({
|
|
110
|
+
url: parts.url,
|
|
111
|
+
body: parts.bodyText,
|
|
112
|
+
headers: parts.headers,
|
|
113
|
+
encoding: input.request.http?.encoding,
|
|
114
|
+
}),
|
|
110
115
|
framing: Framing.sse,
|
|
111
116
|
middleware: input.middleware,
|
|
112
117
|
},
|
|
@@ -9,6 +9,7 @@ import { Protocol } from "../route/protocol.js";
|
|
|
9
9
|
import { AIError, AIErrorReason, InvalidProviderOutputError, LLMEvent, ProviderInternalError, UnknownProviderError, Usage, } from "../schema/index.js";
|
|
10
10
|
import { classifyProviderFailure } from "../provider-error.js";
|
|
11
11
|
import { isRecord, JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js";
|
|
12
|
+
import { cacheControl } from "./utils/cache.js";
|
|
12
13
|
import { OpenAIOptions } from "./utils/openai-options.js";
|
|
13
14
|
import { Lifecycle } from "./utils/lifecycle.js";
|
|
14
15
|
import { ToolStream } from "./utils/tool-stream.js";
|
|
@@ -646,14 +647,15 @@ export const fromRequest = Effect.fn("OpenAIChat.fromRequest")(function* (reques
|
|
|
646
647
|
const zaiToolStream = request.model.compatibility?.zaiToolStream ?? detectZaiToolStream(provider, baseURL, request.model.id);
|
|
647
648
|
const hasHistory = hasToolHistory(request.messages);
|
|
648
649
|
const hasActiveTools = flattened.tools.length > 0;
|
|
650
|
+
const lowering = { ...options, cacheControl: options.cacheControl ?? declaredCacheControl(request) };
|
|
649
651
|
return {
|
|
650
652
|
model: request.model.id,
|
|
651
|
-
messages: yield* lowerMessages(flattened.request,
|
|
653
|
+
messages: yield* lowerMessages(flattened.request, lowering),
|
|
652
654
|
tools: flattened.tools.length === 0
|
|
653
655
|
? hasHistory
|
|
654
656
|
? []
|
|
655
657
|
: undefined
|
|
656
|
-
: flattened.tools.map((tool) => lowerTool(tool,
|
|
658
|
+
: flattened.tools.map((tool) => lowerTool(tool, lowering, supportsStrictMode)),
|
|
657
659
|
tool_choice: hasActiveTools && request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined,
|
|
658
660
|
stream: true,
|
|
659
661
|
...(supportsUsageInStreaming ? { stream_options: { include_usage: true } } : {}),
|
|
@@ -670,6 +672,8 @@ export const fromRequest = Effect.fn("OpenAIChat.fromRequest")(function* (reques
|
|
|
670
672
|
...lowerOptions(request, supportsStore),
|
|
671
673
|
};
|
|
672
674
|
});
|
|
675
|
+
// Provider wrappers that pass their own marker writer keep it; otherwise a declared cache format adds one.
|
|
676
|
+
const declaredCacheControl = (request) => request.model.compatibility?.cacheFormat === undefined ? undefined : cacheControl();
|
|
673
677
|
// =============================================================================
|
|
674
678
|
// Stream Parsing
|
|
675
679
|
// =============================================================================
|
|
@@ -2,7 +2,7 @@ import { Tool } from "@opencode/schema/tool";
|
|
|
2
2
|
import { Effect, Schema } from "effect";
|
|
3
3
|
import { Headers, HttpClientRequest } from "effect/http";
|
|
4
4
|
import { Media } from "../media.js";
|
|
5
|
-
import { AIError, LLMRequest, Message, ToolDefinition, type ContentPart, type MediaPart, type OpenString, type ProviderID, type ToolEntry, type ToolResultPart } from "../schema/index.js";
|
|
5
|
+
import { AIError, LLMRequest, Message, ToolDefinition, type ContentPart, type MediaPart, type OpenString, type ProviderID, type RequestEncoding, type ToolEntry, type ToolResultPart } from "../schema/index.js";
|
|
6
6
|
import { Json, decodeJson, encodeJson } from "../utils/json.js";
|
|
7
7
|
import { isRecord } from "../utils/record.js";
|
|
8
8
|
export { Json, decodeJson, encodeJson, isRecord };
|
|
@@ -14,6 +14,7 @@ export declare const lenient: <const S extends Schema.Top>(schema: S) => Schema.
|
|
|
14
14
|
/** Provider-defined string enum: known values for autocomplete, any string accepted at runtime. */
|
|
15
15
|
export declare const knownString: <Known extends string>() => Schema.declare<OpenString<Known>, OpenString<Known>>;
|
|
16
16
|
export declare const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
|
|
17
|
+
export declare const sendsPromptCacheKey: (request: LLMRequest) => boolean;
|
|
17
18
|
export declare const promptCacheKey: (request: LLMRequest) => string | undefined;
|
|
18
19
|
/**
|
|
19
20
|
* Streaming tool-call accumulator. Adapters that build a tool call across
|
|
@@ -192,10 +193,13 @@ export declare const validateWith: <A, I, E extends {
|
|
|
192
193
|
* send JSON with a stale content type. The body is passed pre-encoded so
|
|
193
194
|
* routes can choose between
|
|
194
195
|
* `Schema.encodeSync(payload)` and `ProviderShared.encodeJson(payload)`.
|
|
196
|
+
* A `zstd` or `gzip` encoding compresses the body and sets `content-encoding`;
|
|
197
|
+
* it falls back to plain JSON when the runtime has no zlib.
|
|
195
198
|
*/
|
|
196
199
|
export declare const jsonPost: (input: {
|
|
197
200
|
readonly url: string;
|
|
198
201
|
readonly body: string;
|
|
199
202
|
readonly headers?: Headers.Input;
|
|
203
|
+
readonly encoding?: RequestEncoding;
|
|
200
204
|
}) => HttpClientRequest.HttpClientRequest;
|
|
201
205
|
export * as ProviderShared from "./shared.js";
|
package/dist/protocols/shared.js
CHANGED
|
@@ -17,10 +17,13 @@ export const knownString = () => Schema.declare((value) => typeof value === "str
|
|
|
17
17
|
expected: "string",
|
|
18
18
|
});
|
|
19
19
|
export const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
|
|
20
|
+
// Body-level `prompt_cache_key` is off when caching is disabled or the model
|
|
21
|
+
// explicitly rejects the field. Header-based affinity is unaffected.
|
|
22
|
+
export const sendsPromptCacheKey = (request) => request.cache !== "none" && request.model.compatibility?.supportsPromptCacheKey !== false;
|
|
20
23
|
// OpenAI limits `prompt_cache_key` to 64 chars; DeepSeek and Zai inherit the same
|
|
21
24
|
// limit via their OpenAI-compatible APIs. Clamp with unicode-aware slicing.
|
|
22
25
|
export const promptCacheKey = (request) => {
|
|
23
|
-
if (request
|
|
26
|
+
if (!sendsPromptCacheKey(request) || request.promptCacheKey === undefined)
|
|
24
27
|
return undefined;
|
|
25
28
|
const chars = Array.from(request.promptCacheKey);
|
|
26
29
|
if (chars.length <= OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH)
|
|
@@ -279,6 +282,29 @@ export const validateWith = (decode) => (payload) => decode(payload).pipe(Effect
|
|
|
279
282
|
* send JSON with a stale content type. The body is passed pre-encoded so
|
|
280
283
|
* routes can choose between
|
|
281
284
|
* `Schema.encodeSync(payload)` and `ProviderShared.encodeJson(payload)`.
|
|
285
|
+
* A `zstd` or `gzip` encoding compresses the body and sets `content-encoding`;
|
|
286
|
+
* it falls back to plain JSON when the runtime has no zlib.
|
|
282
287
|
*/
|
|
283
|
-
export const jsonPost = (input) =>
|
|
288
|
+
export const jsonPost = (input) => {
|
|
289
|
+
const headers = Headers.set(Headers.fromInput(input.headers), "content-type", "application/json");
|
|
290
|
+
const compressed = compressBody(input.body, input.encoding);
|
|
291
|
+
if (!compressed)
|
|
292
|
+
return HttpClientRequest.post(input.url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.bodyText(input.body, "application/json"));
|
|
293
|
+
return HttpClientRequest.post(input.url).pipe(HttpClientRequest.setHeaders(Headers.set(headers, "content-encoding", compressed.encoding)), HttpClientRequest.bodyUint8Array(compressed.body, "application/json"));
|
|
294
|
+
};
|
|
295
|
+
function compressBody(text, encoding) {
|
|
296
|
+
if (encoding === undefined || encoding === "none")
|
|
297
|
+
return undefined;
|
|
298
|
+
// Looked up at call time instead of imported: a runtime without node:zlib or its zstd export
|
|
299
|
+
// (browsers, older Node, some workerd setups) sends plain JSON rather than failing to load this module.
|
|
300
|
+
const zlib = globalThis.process?.getBuiltinModule?.("node:zlib");
|
|
301
|
+
try {
|
|
302
|
+
const body = encoding === "zstd" ? zlib?.zstdCompressSync?.(text) : zlib?.gzipSync(text);
|
|
303
|
+
return body && { encoding, body };
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
// Compression is an optimization, so a failure sends plain JSON instead of failing the request.
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
284
310
|
export * as ProviderShared from "./shared.js";
|
|
@@ -87,7 +87,12 @@ export const make = (adapter, lowerTools) => Effect.fn("ResponsesCompaction.exec
|
|
|
87
87
|
auth: route.auth,
|
|
88
88
|
encodeBody: Schema.encodeSync(Schema.fromJsonString(Body)),
|
|
89
89
|
});
|
|
90
|
-
const response = yield* executor.execute(ProviderShared.jsonPost({
|
|
90
|
+
const response = yield* executor.execute(ProviderShared.jsonPost({
|
|
91
|
+
url: parts.url,
|
|
92
|
+
body: parts.bodyText,
|
|
93
|
+
headers: parts.headers,
|
|
94
|
+
encoding: request.http?.encoding,
|
|
95
|
+
}), options?.http);
|
|
91
96
|
const text = yield* RequestExecutor.responseStream(response).pipe(Stream.decodeText(), Stream.runFold(() => "", (text, chunk) => text + chunk));
|
|
92
97
|
const invalid = (message, cause) => new AIError({
|
|
93
98
|
reason: new InvalidProviderOutputError({
|
|
@@ -129,7 +129,7 @@ export declare const routes: (Route<{
|
|
|
129
129
|
readonly temperature?: number | undefined;
|
|
130
130
|
readonly seed?: number | undefined;
|
|
131
131
|
readonly thinking?: {
|
|
132
|
-
readonly type:
|
|
132
|
+
readonly type: (string & {}) | "disabled" | "adaptive";
|
|
133
133
|
} | undefined;
|
|
134
134
|
readonly tool_choice?: "required" | "auto" | "none" | {
|
|
135
135
|
readonly type: "function";
|
|
@@ -611,7 +611,7 @@ export declare const configure: (input: Config) => {
|
|
|
611
611
|
id: string & import("effect/Brand").Brand<"AI.ProviderID">;
|
|
612
612
|
model: (id: string | ModelID) => import("../schema/options.js").LanguageModel<{
|
|
613
613
|
readonly thinking?: {
|
|
614
|
-
readonly type:
|
|
614
|
+
readonly type: (string & {}) | "disabled" | "adaptive";
|
|
615
615
|
} | undefined;
|
|
616
616
|
readonly thinkingBudget?: number | undefined;
|
|
617
617
|
readonly parallelToolCalls?: boolean | undefined;
|
|
@@ -636,7 +636,7 @@ export declare const configure: (input: Config) => {
|
|
|
636
636
|
}, import("../route/client.js").CompactionOperations | undefined>;
|
|
637
637
|
chat: (id: string | ModelID) => import("../schema/options.js").LanguageModel<{
|
|
638
638
|
readonly thinking?: {
|
|
639
|
-
readonly type:
|
|
639
|
+
readonly type: (string & {}) | "disabled" | "adaptive";
|
|
640
640
|
} | undefined;
|
|
641
641
|
readonly thinkingBudget?: number | undefined;
|
|
642
642
|
readonly parallelToolCalls?: boolean | undefined;
|
|
@@ -675,7 +675,7 @@ export declare const provider: {
|
|
|
675
675
|
id: string & import("effect/Brand").Brand<"AI.ProviderID">;
|
|
676
676
|
model: (id: string | ModelID) => import("../schema/options.js").LanguageModel<{
|
|
677
677
|
readonly thinking?: {
|
|
678
|
-
readonly type:
|
|
678
|
+
readonly type: (string & {}) | "disabled" | "adaptive";
|
|
679
679
|
} | undefined;
|
|
680
680
|
readonly thinkingBudget?: number | undefined;
|
|
681
681
|
readonly parallelToolCalls?: boolean | undefined;
|
|
@@ -700,7 +700,7 @@ export declare const provider: {
|
|
|
700
700
|
}, import("../route/client.js").CompactionOperations | undefined>;
|
|
701
701
|
chat: (id: string | ModelID) => import("../schema/options.js").LanguageModel<{
|
|
702
702
|
readonly thinking?: {
|
|
703
|
-
readonly type:
|
|
703
|
+
readonly type: (string & {}) | "disabled" | "adaptive";
|
|
704
704
|
} | undefined;
|
|
705
705
|
readonly thinkingBudget?: number | undefined;
|
|
706
706
|
readonly parallelToolCalls?: boolean | undefined;
|
|
@@ -109,7 +109,8 @@ export declare const routes: Route<{
|
|
|
109
109
|
} | undefined;
|
|
110
110
|
readonly maxOutputTokens?: number | undefined;
|
|
111
111
|
} | undefined;
|
|
112
|
-
|
|
112
|
+
readonly continuationToken?: string | undefined;
|
|
113
|
+
}, Gemini.Prepared, import("../route/client.js").CompactionOperations | undefined>[];
|
|
113
114
|
export declare const configure: (input?: Config) => {
|
|
114
115
|
id: string & import("effect/Brand").Brand<"AI.ProviderID">;
|
|
115
116
|
model: (modelID: string | ModelID) => import("../schema/options.js").LanguageModel<GeminiOptionsInput, import("../route/client.js").CompactionOperations | undefined>;
|
|
@@ -4,7 +4,6 @@ import { ProviderShared } from "../protocols/shared.js";
|
|
|
4
4
|
import { Auth } from "../route/auth.js";
|
|
5
5
|
import { Route } from "../route/client.js";
|
|
6
6
|
import { Endpoint } from "../route/endpoint.js";
|
|
7
|
-
import { Framing } from "../route/framing.js";
|
|
8
7
|
import { ProviderConfigurationError, ProviderID } from "../schema/index.js";
|
|
9
8
|
import { GoogleVertexShared } from "./google-vertex-shared.js";
|
|
10
9
|
export const id = ProviderID.make("google-vertex");
|
|
@@ -37,7 +36,7 @@ const route = Route.make({
|
|
|
37
36
|
const serviceTier = request.providerOptions?.serviceTier;
|
|
38
37
|
return typeof serviceTier === "string" ? { "x-vertex-ai-llm-shared-request-type": serviceTier } : {};
|
|
39
38
|
},
|
|
40
|
-
|
|
39
|
+
transport: Gemini.transport,
|
|
41
40
|
});
|
|
42
41
|
export const routes = [route];
|
|
43
42
|
const configuredRoute = (input, modelID) => {
|
|
@@ -96,7 +96,8 @@ export declare const routes: (import("../route/client.js").Route<{
|
|
|
96
96
|
} | undefined;
|
|
97
97
|
readonly maxOutputTokens?: number | undefined;
|
|
98
98
|
} | undefined;
|
|
99
|
-
|
|
99
|
+
readonly continuationToken?: string | undefined;
|
|
100
|
+
}, Gemini.Prepared, import("../route/client.js").CompactionOperations | undefined> | import("../route/client.js").Route<{
|
|
100
101
|
readonly input: readonly ({
|
|
101
102
|
readonly type: "user_input" | "model_output";
|
|
102
103
|
readonly content: readonly ({
|
|
@@ -34,7 +34,7 @@ export * as Moonshot from "./moonshot.js";
|
|
|
34
34
|
export * as OpenAI from "./openai.js";
|
|
35
35
|
export * as OpenAICompatible from "./openai-compatible.js";
|
|
36
36
|
export * as OpenAICompatibleResponses from "./openai-compatible-responses.js";
|
|
37
|
-
export * as
|
|
37
|
+
export * as OpenCode from "./opencode.js";
|
|
38
38
|
export * as OpenRouter from "./openrouter.js";
|
|
39
39
|
export * as Replicate from "./replicate.js";
|
|
40
40
|
export * as Runway from "./runway.js";
|
package/dist/providers/index.js
CHANGED
|
@@ -34,7 +34,7 @@ export * as Moonshot from "./moonshot.js";
|
|
|
34
34
|
export * as OpenAI from "./openai.js";
|
|
35
35
|
export * as OpenAICompatible from "./openai-compatible.js";
|
|
36
36
|
export * as OpenAICompatibleResponses from "./openai-compatible-responses.js";
|
|
37
|
-
export * as
|
|
37
|
+
export * as OpenCode from "./opencode.js";
|
|
38
38
|
export * as OpenRouter from "./openrouter.js";
|
|
39
39
|
export * as Replicate from "./replicate.js";
|
|
40
40
|
export * as Runway from "./runway.js";
|
|
@@ -29,4 +29,4 @@ export declare const provider: {
|
|
|
29
29
|
export declare const experimental: {
|
|
30
30
|
evaluation: (modelID: string | ModelID) => import("../experimental.js").EvaluationModel<import("../experimental.js").EvaluationOptions>;
|
|
31
31
|
};
|
|
32
|
-
export * as
|
|
32
|
+
export * as OpenCode from "./opencode.js";
|
|
@@ -2,7 +2,7 @@ import { HttpOptions, ProviderID } from "../schema/index.js";
|
|
|
2
2
|
import { AuthOptions } from "../route/auth-options.js";
|
|
3
3
|
import { SystemOne } from "../experimental/system-one.js";
|
|
4
4
|
export const id = ProviderID.make("opencode");
|
|
5
|
-
const baseURL = "https://opencode.ai/
|
|
5
|
+
const baseURL = "https://opencode.ai/inference/v1";
|
|
6
6
|
export const configure = (input = {}) => {
|
|
7
7
|
const evaluation = (modelID) => SystemOne.model({
|
|
8
8
|
id: modelID,
|
|
@@ -17,4 +17,4 @@ export const configure = (input = {}) => {
|
|
|
17
17
|
};
|
|
18
18
|
export const provider = configure();
|
|
19
19
|
export const experimental = provider.experimental;
|
|
20
|
-
export * as
|
|
20
|
+
export * as OpenCode from "./opencode.js";
|
|
@@ -4,7 +4,7 @@ import { Auth } from "../auth.js";
|
|
|
4
4
|
import { render as renderEndpoint } from "../endpoint.js";
|
|
5
5
|
import { Framing } from "../framing.js";
|
|
6
6
|
import * as ProviderShared from "../../protocols/shared.js";
|
|
7
|
-
import { AIError, DEFAULT_HTTP_TIMEOUT_MS, mergeJsonRecords, TransportError, } from "../../schema/index.js";
|
|
7
|
+
import { AIError, DEFAULT_CHUNK_TIMEOUT_MS, DEFAULT_HTTP_TIMEOUT_MS, mergeJsonRecords, TransportError, } from "../../schema/index.js";
|
|
8
8
|
import { RequestExecutor } from "../executor.js";
|
|
9
9
|
const applyQuery = (url, query) => {
|
|
10
10
|
if (!query)
|
|
@@ -46,6 +46,7 @@ export const httpJson = (input) => ({
|
|
|
46
46
|
url: parts.url,
|
|
47
47
|
body: parts.bodyText,
|
|
48
48
|
headers: parts.headers,
|
|
49
|
+
encoding: prepareInput.request.http?.encoding,
|
|
49
50
|
});
|
|
50
51
|
return {
|
|
51
52
|
request,
|
|
@@ -75,7 +76,7 @@ export const httpJson = (input) => ({
|
|
|
75
76
|
const remaining = Duration.subtract(total, Duration.millis((yield* Clock.currentTimeMillis) - started));
|
|
76
77
|
return {
|
|
77
78
|
frames: prepared.framing.frame(RequestExecutor.responseStream(response).pipe(Stream.timeoutOrElse({
|
|
78
|
-
duration: timeoutDuration(request.http?.chunkTimeout),
|
|
79
|
+
duration: timeoutDuration(request.http?.chunkTimeout, DEFAULT_CHUNK_TIMEOUT_MS),
|
|
79
80
|
orElse: () => Stream.fail(timeout("read", "Timed out waiting for response data", http)),
|
|
80
81
|
}), Stream.interruptWhen(Effect.sleep(remaining).pipe(Effect.andThen(Effect.fail(timeout("read", "Timed out waiting for the response to complete", http))))))),
|
|
81
82
|
http,
|
|
@@ -83,7 +84,7 @@ export const httpJson = (input) => ({
|
|
|
83
84
|
};
|
|
84
85
|
}),
|
|
85
86
|
});
|
|
86
|
-
const timeoutDuration = (value) => value === false ? Duration.infinity : Duration.millis(value ??
|
|
87
|
+
const timeoutDuration = (value, fallback = DEFAULT_HTTP_TIMEOUT_MS) => value === false ? Duration.infinity : Duration.millis(value ?? fallback);
|
|
87
88
|
export const sseJson = {
|
|
88
89
|
id: "http-json/sse",
|
|
89
90
|
with: () => httpJson({ framing: Framing.sse }),
|
package/dist/schema/options.d.ts
CHANGED
|
@@ -12,8 +12,16 @@ export declare const mergeProviderOptions: (...items: ReadonlyArray<ProviderOpti
|
|
|
12
12
|
/** Milliseconds for an HTTP timeout, or `false` to disable it. */
|
|
13
13
|
export declare const HttpTimeout: Schema.Union<readonly [Schema.Number, Schema.Literal<false>]>;
|
|
14
14
|
export type HttpTimeout = Schema.Schema.Type<typeof HttpTimeout>;
|
|
15
|
-
/** Applied to `headerTimeout`
|
|
15
|
+
/** Applied to `headerTimeout` when a request leaves it unset. */
|
|
16
16
|
export declare const DEFAULT_HTTP_TIMEOUT_MS = 300000;
|
|
17
|
+
/**
|
|
18
|
+
* Applied to `chunkTimeout` when a request leaves it unset. Some providers, such as DeepSeek, have specifically asked
|
|
19
|
+
* clients to allow at least 15 minutes, because their models can stream nothing for minutes while they reason.
|
|
20
|
+
*/
|
|
21
|
+
export declare const DEFAULT_CHUNK_TIMEOUT_MS = 900000;
|
|
22
|
+
/** `Content-Encoding` for JSON request bodies; `none` sends plain JSON. */
|
|
23
|
+
export declare const RequestEncoding: Schema.Literals<readonly ["zstd", "gzip", "none"]>;
|
|
24
|
+
export type RequestEncoding = Schema.Schema.Type<typeof RequestEncoding>;
|
|
17
25
|
declare const HttpOptions_base: Schema.Class<HttpOptions, Schema.Struct<{
|
|
18
26
|
readonly body: Schema.optional<Schema.$Record<Schema.String, Schema.Unknown>>;
|
|
19
27
|
readonly headers: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
|
|
@@ -24,6 +32,8 @@ declare const HttpOptions_base: Schema.Class<HttpOptions, Schema.Struct<{
|
|
|
24
32
|
readonly headerTimeout: Schema.optional<Schema.Union<readonly [Schema.Number, Schema.Literal<false>]>>;
|
|
25
33
|
/** Time allowed between streamed response chunks once headers have arrived. */
|
|
26
34
|
readonly chunkTimeout: Schema.optional<Schema.Union<readonly [Schema.Number, Schema.Literal<false>]>>;
|
|
35
|
+
/** Compression for JSON request bodies. Only set it for endpoints known to decode it. */
|
|
36
|
+
readonly encoding: Schema.optional<Schema.Literals<readonly ["zstd", "gzip", "none"]>>;
|
|
27
37
|
}>, {}>;
|
|
28
38
|
export declare class HttpOptions extends HttpOptions_base {
|
|
29
39
|
}
|
|
@@ -89,6 +99,8 @@ export declare const LanguageModelSanitizerCompatibility: Schema.Literals<readon
|
|
|
89
99
|
export type LanguageModelSanitizerCompatibility = Schema.Schema.Type<typeof LanguageModelSanitizerCompatibility>;
|
|
90
100
|
export declare const LanguageModelMaxTokensFieldCompatibility: Schema.Literals<readonly ["max_completion_tokens", "max_tokens"]>;
|
|
91
101
|
export type LanguageModelMaxTokensFieldCompatibility = Schema.Schema.Type<typeof LanguageModelMaxTokensFieldCompatibility>;
|
|
102
|
+
export declare const LanguageModelCacheFormatCompatibility: Schema.Literals<readonly ["anthropic", "alibaba"]>;
|
|
103
|
+
export type LanguageModelCacheFormatCompatibility = Schema.Schema.Type<typeof LanguageModelCacheFormatCompatibility>;
|
|
92
104
|
declare const LanguageModelCompatibility_base: Schema.Class<LanguageModelCompatibility, Schema.Struct<{
|
|
93
105
|
readonly sanitizer: Schema.optional<Schema.Literals<readonly ["gemini", "moonshot", "none"]>>;
|
|
94
106
|
readonly reasoningField: Schema.optional<Schema.String>;
|
|
@@ -101,6 +113,8 @@ declare const LanguageModelCompatibility_base: Schema.Class<LanguageModelCompati
|
|
|
101
113
|
readonly supportsUsageInStreaming: Schema.optional<Schema.Boolean>;
|
|
102
114
|
readonly supportsStrictMode: Schema.optional<Schema.Boolean>;
|
|
103
115
|
readonly supportsPromptCacheKey: Schema.optional<Schema.Boolean>;
|
|
116
|
+
/** Prompt-cache marker syntax the endpoint accepts. Overrides the route default; both send `cache_control`, and `alibaba` skips tool definitions. */
|
|
117
|
+
readonly cacheFormat: Schema.optional<Schema.Literals<readonly ["anthropic", "alibaba"]>>;
|
|
104
118
|
readonly zaiToolStream: Schema.optional<Schema.Boolean>;
|
|
105
119
|
readonly requireSignature: Schema.optional<Schema.Boolean>;
|
|
106
120
|
/** Supports Anthropic's thinking-prefix mismatch controls. Overrides model-ID detection. */
|
package/dist/schema/options.js
CHANGED
|
@@ -34,8 +34,15 @@ export const ProviderMetadata = Schema.Record(Schema.String, Schema.Record(Schem
|
|
|
34
34
|
export const mergeProviderOptions = (...items) => mergeJsonRecords(...items);
|
|
35
35
|
/** Milliseconds for an HTTP timeout, or `false` to disable it. */
|
|
36
36
|
export const HttpTimeout = Schema.Union([Schema.Number.check(Schema.isGreaterThan(0)), Schema.Literal(false)]);
|
|
37
|
-
/** Applied to `headerTimeout`
|
|
37
|
+
/** Applied to `headerTimeout` when a request leaves it unset. */
|
|
38
38
|
export const DEFAULT_HTTP_TIMEOUT_MS = 300_000;
|
|
39
|
+
/**
|
|
40
|
+
* Applied to `chunkTimeout` when a request leaves it unset. Some providers, such as DeepSeek, have specifically asked
|
|
41
|
+
* clients to allow at least 15 minutes, because their models can stream nothing for minutes while they reason.
|
|
42
|
+
*/
|
|
43
|
+
export const DEFAULT_CHUNK_TIMEOUT_MS = 900_000;
|
|
44
|
+
/** `Content-Encoding` for JSON request bodies; `none` sends plain JSON. */
|
|
45
|
+
export const RequestEncoding = Schema.Literals(["zstd", "gzip", "none"]);
|
|
39
46
|
export class HttpOptions extends Schema.Class("AI.HttpOptions")({
|
|
40
47
|
body: Schema.optional(JsonSchema),
|
|
41
48
|
headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
|
@@ -46,6 +53,8 @@ export class HttpOptions extends Schema.Class("AI.HttpOptions")({
|
|
|
46
53
|
headerTimeout: Schema.optional(HttpTimeout),
|
|
47
54
|
/** Time allowed between streamed response chunks once headers have arrived. */
|
|
48
55
|
chunkTimeout: Schema.optional(HttpTimeout),
|
|
56
|
+
/** Compression for JSON request bodies. Only set it for endpoints known to decode it. */
|
|
57
|
+
encoding: Schema.optional(RequestEncoding),
|
|
49
58
|
}) {
|
|
50
59
|
}
|
|
51
60
|
(function (HttpOptions) {
|
|
@@ -63,9 +72,16 @@ export const mergeHttpOptions = (...items) => {
|
|
|
63
72
|
const timeout = items.findLast((item) => item?.timeout !== undefined)?.timeout;
|
|
64
73
|
const headerTimeout = items.findLast((item) => item?.headerTimeout !== undefined)?.headerTimeout;
|
|
65
74
|
const chunkTimeout = items.findLast((item) => item?.chunkTimeout !== undefined)?.chunkTimeout;
|
|
66
|
-
|
|
75
|
+
const encoding = items.findLast((item) => item?.encoding !== undefined)?.encoding;
|
|
76
|
+
if (!body &&
|
|
77
|
+
!headers &&
|
|
78
|
+
!query &&
|
|
79
|
+
timeout === undefined &&
|
|
80
|
+
headerTimeout === undefined &&
|
|
81
|
+
chunkTimeout === undefined &&
|
|
82
|
+
encoding === undefined)
|
|
67
83
|
return undefined;
|
|
68
|
-
return new HttpOptions({ body, headers, query, timeout, headerTimeout, chunkTimeout });
|
|
84
|
+
return new HttpOptions({ body, headers, query, timeout, headerTimeout, chunkTimeout, encoding });
|
|
69
85
|
};
|
|
70
86
|
export class GenerationOptions extends Schema.Class("LLM.GenerationOptions")({
|
|
71
87
|
maxTokens: Schema.optional(Schema.Number),
|
|
@@ -119,6 +135,7 @@ export const ReasoningEffort = Schema.declare((value) => typeof value === "strin
|
|
|
119
135
|
/** Tool schema sanitizer for a model family. `none` opts out of the protocol and model-name defaults. */
|
|
120
136
|
export const LanguageModelSanitizerCompatibility = Schema.Literals(["gemini", "moonshot", "none"]);
|
|
121
137
|
export const LanguageModelMaxTokensFieldCompatibility = Schema.Literals(["max_completion_tokens", "max_tokens"]);
|
|
138
|
+
export const LanguageModelCacheFormatCompatibility = Schema.Literals(["anthropic", "alibaba"]);
|
|
122
139
|
export class LanguageModelCompatibility extends Schema.Class("LLM.LanguageModelCompatibility")({
|
|
123
140
|
sanitizer: Schema.optional(LanguageModelSanitizerCompatibility),
|
|
124
141
|
reasoningField: Schema.optional(Schema.String),
|
|
@@ -130,9 +147,12 @@ export class LanguageModelCompatibility extends Schema.Class("LLM.LanguageModelC
|
|
|
130
147
|
supportsStore: Schema.optional(Schema.Boolean),
|
|
131
148
|
supportsUsageInStreaming: Schema.optional(Schema.Boolean),
|
|
132
149
|
supportsStrictMode: Schema.optional(Schema.Boolean),
|
|
133
|
-
// Accepts `prompt_cache_key` in the
|
|
134
|
-
//
|
|
150
|
+
// Accepts `prompt_cache_key` in the request body. Chat omits the key unless
|
|
151
|
+
// this is true; other protocols omit it only when this is false.
|
|
152
|
+
// Session-affinity headers still flow regardless.
|
|
135
153
|
supportsPromptCacheKey: Schema.optional(Schema.Boolean),
|
|
154
|
+
/** Prompt-cache marker syntax the endpoint accepts. Overrides the route default; both send `cache_control`, and `alibaba` skips tool definitions. */
|
|
155
|
+
cacheFormat: Schema.optional(LanguageModelCacheFormatCompatibility),
|
|
136
156
|
zaiToolStream: Schema.optional(Schema.Boolean),
|
|
137
157
|
requireSignature: Schema.optional(Schema.Boolean),
|
|
138
158
|
/** Supports Anthropic's thinking-prefix mismatch controls. Overrides model-ID detection. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.29",
|
|
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.1",
|
|
37
|
-
"@opencode/http-recorder": "2.0.
|
|
37
|
+
"@opencode/http-recorder": "2.0.29",
|
|
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": "2.0.
|
|
47
|
+
"@opencode/schema": "2.0.29",
|
|
48
48
|
"aws4fetch": "1.0.20",
|
|
49
49
|
"effect": "4.0.1",
|
|
50
50
|
"google-auth-library": "10.5.0"
|