@opencode/ai 2.1.0 → 2.1.2
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/dist/experimental/realtime.js +11 -6
- package/dist/experimental/system-one.js +8 -8
- package/dist/media.js +3 -3
- package/dist/protocols/venice-chat.js +6 -6
- package/dist/provider-error.js +1 -1
- package/dist/providers/vercel-ai-gateway.js +7 -6
- package/dist/route/client.js +1 -2
- package/dist/route/executor-service.d.ts +6 -2
- package/dist/route/executor-service.js +7 -0
- package/dist/route/executor.d.ts +2 -3
- package/dist/route/executor.js +6 -11
- package/dist/route/media-protocol.d.ts +1 -1
- package/dist/route/media-protocol.js +3 -3
- package/dist/route/transport/websocket.d.ts +0 -24
- package/dist/route/transport/websocket.js +0 -50
- package/dist/schema/errors.d.ts +8 -7
- package/dist/schema/errors.js +8 -4
- package/package.json +3 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Clock, Effect, Redacted, Schema } from "effect";
|
|
2
2
|
import { HttpClientRequest } from "effect/http";
|
|
3
3
|
import { RequestExecutor } from "../route/executor.js";
|
|
4
|
-
import { AIError,
|
|
4
|
+
import { AIError, InvalidProviderOutputError, InvalidRequestError } from "../schema/index.js";
|
|
5
5
|
import { encodeJson } from "../utils/json.js";
|
|
6
6
|
const OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
7
7
|
const XAI_BASE_URL = "https://api.x.ai/v1";
|
|
@@ -28,7 +28,10 @@ export const connect = Effect.fn("Realtime.connect")(function* (input) {
|
|
|
28
28
|
}
|
|
29
29
|
case "openai-realtime": {
|
|
30
30
|
if (input.transport.type === "websocket")
|
|
31
|
-
return yield* clientSecret(resolved.id, OPENAI_BASE_URL, input.model, input.apiKey,
|
|
31
|
+
return yield* clientSecret(resolved.id, OPENAI_BASE_URL, input.model, input.apiKey, {
|
|
32
|
+
expires_after: { anchor: "created_at", seconds: 60 },
|
|
33
|
+
session,
|
|
34
|
+
});
|
|
32
35
|
const form = new FormData();
|
|
33
36
|
form.append("sdp", input.transport.sdp);
|
|
34
37
|
form.append("session", encodeJson(session));
|
|
@@ -39,7 +42,9 @@ export const connect = Effect.fn("Realtime.connect")(function* (input) {
|
|
|
39
42
|
case "xai-realtime": {
|
|
40
43
|
if (input.transport.type !== "websocket")
|
|
41
44
|
return yield* invalidRequest("xAI Grok Voice only supports WebSocket transport");
|
|
42
|
-
return yield* clientSecret(resolved.id, XAI_BASE_URL, input.model, input.apiKey
|
|
45
|
+
return yield* clientSecret(resolved.id, XAI_BASE_URL, input.model, input.apiKey, {
|
|
46
|
+
expires_after: { seconds: 60 },
|
|
47
|
+
});
|
|
43
48
|
}
|
|
44
49
|
case "gemini-live": {
|
|
45
50
|
if (input.transport.type !== "websocket")
|
|
@@ -69,7 +74,7 @@ const LiveSessionResponse = Schema.fromJsonString(Schema.Struct({
|
|
|
69
74
|
transport: Schema.Struct({ sdp: Schema.String }),
|
|
70
75
|
}));
|
|
71
76
|
// xAI's ephemeral-token docs (https://docs.x.ai/developers/model-capabilities/audio/ephemeral-tokens)
|
|
72
|
-
// document the request body (`{ expires_after: { seconds } }`) but not the response payload; we assume
|
|
77
|
+
// document the request body (`{ expires_after: { seconds } }`, no `anchor` or `session`) but not the response payload; we assume
|
|
73
78
|
// it mirrors OpenAI's `{ value, expires_at }` response shape.
|
|
74
79
|
const ClientSecretResponse = Schema.fromJsonString(Schema.Struct({
|
|
75
80
|
value: Schema.String,
|
|
@@ -86,12 +91,12 @@ const post = (route, url, apiKey, body, response, authHeader) => Effect.gen(func
|
|
|
86
91
|
: HttpClientRequest.bearerToken(apiKey), body instanceof FormData
|
|
87
92
|
? HttpClientRequest.bodyFormData(body)
|
|
88
93
|
: HttpClientRequest.bodyText(encodeJson(body), "application/json")));
|
|
89
|
-
const http =
|
|
94
|
+
const http = RequestExecutor.responseHttp(res);
|
|
90
95
|
const fail = (message, cause, raw) => new AIError({ reason: new InvalidProviderOutputError({ route, message, body: raw, http, cause }) });
|
|
91
96
|
const text = yield* res.text.pipe(Effect.mapError((cause) => fail(`Failed to read ${route} response`, cause)));
|
|
92
97
|
return yield* Schema.decodeEffect(response)(text).pipe(Effect.mapError((cause) => fail(`${route} returned an invalid response`, cause, text)));
|
|
93
98
|
});
|
|
94
|
-
const clientSecret = (route, baseURL, model, apiKey,
|
|
99
|
+
const clientSecret = (route, baseURL, model, apiKey, body) => post(route, `${baseURL}/realtime/client_secrets`, apiKey, body, ClientSecretResponse).pipe(Effect.map((data) => ({
|
|
95
100
|
type: "websocket",
|
|
96
101
|
url: `${baseURL.replace(/^http/, "ws")}/realtime?model=${encodeURIComponent(model)}`,
|
|
97
102
|
token: data.value,
|
|
@@ -2,7 +2,9 @@ import { Effect, Schema } from "effect";
|
|
|
2
2
|
import { Headers, HttpClientRequest } from "effect/http";
|
|
3
3
|
import { ChoiceQuestion, EvaluationInput, EvaluationModel, EvaluationResponse, EvaluationRounding, ScoreQuestion, } from "./evaluation.js";
|
|
4
4
|
import { Auth } from "../route/auth.js";
|
|
5
|
-
import {
|
|
5
|
+
import { RequestExecutor } from "../route/executor.js";
|
|
6
|
+
import { JsonObject, ProviderShared } from "../protocols/shared.js";
|
|
7
|
+
import { AIError, HttpOptions, InvalidProviderOutputError, InvalidRequestError, ModelID, ProviderMetadata, Usage, mergeJsonRecords, } from "../schema/index.js";
|
|
6
8
|
const Noul = Schema.Struct({
|
|
7
9
|
type: Schema.Literal("noul"),
|
|
8
10
|
instructions: EvaluationInput,
|
|
@@ -24,7 +26,7 @@ const Request = Schema.StructWithRest(Schema.Struct({
|
|
|
24
26
|
model: Schema.String,
|
|
25
27
|
state: EvaluationInput,
|
|
26
28
|
questions: Schema.Record(Schema.String, Question),
|
|
27
|
-
}), [
|
|
29
|
+
}), [JsonObject]);
|
|
28
30
|
const Probability = Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 1 }));
|
|
29
31
|
const NoulAnswer = Schema.Struct({ type: Schema.Literal("noul"), noul: Probability });
|
|
30
32
|
const Choice = Schema.Struct({
|
|
@@ -44,14 +46,14 @@ const Answer = Schema.Union([NoulAnswer, Choice, Score]).pipe(Schema.toTaggedUni
|
|
|
44
46
|
const NativeUsage = Schema.StructWithRest(Schema.Struct({
|
|
45
47
|
input_tokens: Schema.optional(Schema.Number),
|
|
46
48
|
output_tokens: Schema.optional(Schema.Number),
|
|
47
|
-
}), [
|
|
49
|
+
}), [JsonObject]);
|
|
48
50
|
const Response = Schema.Struct({
|
|
49
51
|
model: Schema.String,
|
|
50
52
|
answers: Schema.Record(Schema.String, Answer),
|
|
51
53
|
usage: Schema.optional(NativeUsage),
|
|
52
54
|
id: Schema.optional(Schema.String),
|
|
53
55
|
provider: Schema.optional(Schema.String),
|
|
54
|
-
provider_metadata: Schema.optional(
|
|
56
|
+
provider_metadata: Schema.optional(ProviderMetadata),
|
|
55
57
|
});
|
|
56
58
|
export const model = (cfg) => EvaluationModel.make({
|
|
57
59
|
id: cfg.id,
|
|
@@ -76,7 +78,7 @@ export const model = (cfg) => EvaluationModel.make({
|
|
|
76
78
|
headers: Headers.fromInput({ ...cfg.headers, ...req.http?.headers }),
|
|
77
79
|
});
|
|
78
80
|
const res = yield* send(HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.bodyText(body, "application/json")));
|
|
79
|
-
const http =
|
|
81
|
+
const http = RequestExecutor.responseHttp(res);
|
|
80
82
|
const fail = (message, cause, body) => new AIError({ reason: new InvalidProviderOutputError({ route: "system-one", message, body, http, cause }) });
|
|
81
83
|
const text = yield* res.text.pipe(Effect.mapError((cause) => fail("Failed to read the System One response", cause)));
|
|
82
84
|
const data = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Response))(text).pipe(Effect.mapError((cause) => fail("System One returned an invalid response", cause, text)));
|
|
@@ -120,9 +122,7 @@ export const model = (cfg) => EvaluationModel.make({
|
|
|
120
122
|
? new Usage({
|
|
121
123
|
inputTokens: data.usage.input_tokens,
|
|
122
124
|
outputTokens: data.usage.output_tokens,
|
|
123
|
-
totalTokens: data.usage.input_tokens
|
|
124
|
-
? undefined
|
|
125
|
-
: (data.usage.input_tokens ?? 0) + (data.usage.output_tokens ?? 0),
|
|
125
|
+
totalTokens: ProviderShared.totalTokens(data.usage.input_tokens, data.usage.output_tokens, undefined),
|
|
126
126
|
providerMetadata: { [cfg.providerMetadataKey]: data.usage },
|
|
127
127
|
})
|
|
128
128
|
: undefined,
|
package/dist/media.js
CHANGED
|
@@ -3,9 +3,9 @@ import { Effect, FileSystem, Schema, SchemaGetter } from "effect";
|
|
|
3
3
|
import { Base64 } from "effect/encoding";
|
|
4
4
|
import { HttpClientRequest } from "effect/http";
|
|
5
5
|
import { ProviderID } from "./schema/ids.js";
|
|
6
|
-
import { AIError,
|
|
6
|
+
import { AIError, InvalidProviderOutputError, InvalidRequestError } from "./schema/errors.js";
|
|
7
7
|
import { ProviderMetadata } from "./schema/options.js";
|
|
8
|
-
import { Service } from "./route/executor-service.js";
|
|
8
|
+
import { responseHttp, Service } from "./route/executor-service.js";
|
|
9
9
|
import { detectMediaType, fileMediaType } from "./utils/media-type.js";
|
|
10
10
|
export { detectMediaType } from "./utils/media-type.js";
|
|
11
11
|
const OCTET_STREAM = "application/octet-stream";
|
|
@@ -183,7 +183,7 @@ const download = Effect.fn("Media.download")(function* (source, headers) {
|
|
|
183
183
|
const buffer = yield* response.arrayBuffer.pipe(Effect.mapError((cause) => new AIError({
|
|
184
184
|
reason: new InvalidProviderOutputError({
|
|
185
185
|
message: `Failed to read media from ${source.url}`,
|
|
186
|
-
http:
|
|
186
|
+
http: responseHttp(response),
|
|
187
187
|
cause,
|
|
188
188
|
}),
|
|
189
189
|
})));
|
|
@@ -49,7 +49,7 @@ const Assistant = Schema.StructWithRest(Schema.Struct({
|
|
|
49
49
|
...OpenAIChat.OpenAIChatMessage.cases.assistant.schema.fields,
|
|
50
50
|
tool_calls: optionalArray(ToolCall),
|
|
51
51
|
thought_signature: Schema.optional(Schema.String),
|
|
52
|
-
}), [
|
|
52
|
+
}), [JsonObject]);
|
|
53
53
|
const Messages = Schema.Array(Schema.Union([
|
|
54
54
|
OpenAIChat.OpenAIChatMessage.cases.system,
|
|
55
55
|
OpenAIChat.OpenAIChatMessage.cases.user,
|
|
@@ -85,16 +85,16 @@ const Delta = Schema.StructWithRest(Schema.Struct({
|
|
|
85
85
|
...OpenAIChat.OpenAIChatDelta.schema.fields,
|
|
86
86
|
tool_calls: optionalNull(Schema.Array(ToolDelta)),
|
|
87
87
|
thought_signature: optionalNull(Schema.String),
|
|
88
|
-
}), [
|
|
89
|
-
const Choice = Schema.StructWithRest(Schema.Struct({ ...OpenAIChat.OpenAIChatChoice.schema.fields, delta: optionalNull(Delta) }), [
|
|
88
|
+
}), [JsonObject]);
|
|
89
|
+
const Choice = Schema.StructWithRest(Schema.Struct({ ...OpenAIChat.OpenAIChatChoice.schema.fields, delta: optionalNull(Delta) }), [JsonObject]);
|
|
90
90
|
const Usage = Schema.StructWithRest(Schema.Struct({
|
|
91
91
|
...OpenAIChat.OpenAIChatUsage.schema.fields,
|
|
92
92
|
prompt_tokens_details: optionalNull(Schema.StructWithRest(Schema.Struct({
|
|
93
93
|
cached_tokens: optionalNull(Schema.Number),
|
|
94
94
|
cache_write_tokens: optionalNull(Schema.Number),
|
|
95
95
|
cache_creation_input_tokens: optionalNull(Schema.Number),
|
|
96
|
-
}), [
|
|
97
|
-
}), [
|
|
96
|
+
}), [JsonObject])),
|
|
97
|
+
}), [JsonObject]);
|
|
98
98
|
const Event = Schema.StructWithRest(Schema.Struct({
|
|
99
99
|
...OpenAIChat.OpenAIChatEvent.schema.fields,
|
|
100
100
|
choices: optionalNull(Schema.Array(Choice)),
|
|
@@ -104,7 +104,7 @@ const Event = Schema.StructWithRest(Schema.Struct({
|
|
|
104
104
|
message: Schema.String,
|
|
105
105
|
path: Schema.optional(Schema.Array(Schema.Union([Schema.String, Schema.Number]))),
|
|
106
106
|
})),
|
|
107
|
-
}), [
|
|
107
|
+
}), [JsonObject]);
|
|
108
108
|
const MARKER = "__ENCRYPTED_REASONING__";
|
|
109
109
|
// ---------------------------------------------------------------------------
|
|
110
110
|
// Request lowering
|
package/dist/provider-error.js
CHANGED
|
@@ -201,7 +201,7 @@ export function classifyProviderFailure(input) {
|
|
|
201
201
|
if (input.status === 402 ||
|
|
202
202
|
codes.some((code) => QUOTA_CODES.has(code)) ||
|
|
203
203
|
(input.status === 429 && QUOTA_TEXT.test(text)))
|
|
204
|
-
return new QuotaExceededError(details);
|
|
204
|
+
return new QuotaExceededError({ ...details, retryAfterMs: input.retryAfterMs });
|
|
205
205
|
if (input.status === 401 ||
|
|
206
206
|
input.status === 403 ||
|
|
207
207
|
codes.some((code) => AUTH_CODES.has(code)) ||
|
|
@@ -4,19 +4,20 @@ import { EvaluationAnswer, EvaluationInput, EvaluationModel, EvaluationQuestion,
|
|
|
4
4
|
import { AnthropicMessages } from "../protocols/anthropic-messages.js";
|
|
5
5
|
import { OpenAIChat } from "../protocols/openai-chat.js";
|
|
6
6
|
import { OpenResponses } from "../protocols/open-responses.js";
|
|
7
|
-
import { optionalNull, ProviderShared } from "../protocols/shared.js";
|
|
7
|
+
import { JsonObject, optionalNull, ProviderShared } from "../protocols/shared.js";
|
|
8
8
|
import { gatewayProtocol } from "../protocols/utils/gateway-protocol.js";
|
|
9
9
|
import { Auth } from "../route/auth.js";
|
|
10
10
|
import { AuthOptions } from "../route/auth-options.js";
|
|
11
11
|
import { Route } from "../route/client.js";
|
|
12
12
|
import { Endpoint } from "../route/endpoint.js";
|
|
13
|
+
import { RequestExecutor } from "../route/executor.js";
|
|
13
14
|
import { Framing } from "../route/framing.js";
|
|
14
|
-
import { AIError,
|
|
15
|
+
import { AIError, HttpOptions, InvalidProviderOutputError, InvalidRequestError, LLMRequest, ModelID, ProviderID, ProviderMetadata, ProviderOptions, ReasoningEffort, Usage, } from "../schema/index.js";
|
|
15
16
|
export const id = ProviderID.make("vercel-ai-gateway");
|
|
16
17
|
const baseURL = "https://ai-gateway.vercel.sh/v1";
|
|
17
18
|
const GatewayOptionsSchema = Schema.Struct({
|
|
18
19
|
gateway: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
19
|
-
upstream: Schema.optional(
|
|
20
|
+
upstream: Schema.optional(ProviderMetadata),
|
|
20
21
|
reasoningEffort: Schema.optional(ReasoningEffort),
|
|
21
22
|
thinking: Schema.optional(Schema.Struct({
|
|
22
23
|
type: Schema.String,
|
|
@@ -110,8 +111,8 @@ const Request = Schema.StructWithRest(Schema.Struct({
|
|
|
110
111
|
model: Schema.String,
|
|
111
112
|
state: EvaluationInput,
|
|
112
113
|
questions: Schema.Record(Schema.String, EvaluationQuestion),
|
|
113
|
-
providerOptions: Schema.optional(
|
|
114
|
-
}), [
|
|
114
|
+
providerOptions: Schema.optional(ProviderOptions),
|
|
115
|
+
}), [JsonObject]);
|
|
115
116
|
const Response = Schema.Struct({
|
|
116
117
|
model: optionalNull(Schema.String),
|
|
117
118
|
answers: Schema.Record(Schema.String, EvaluationAnswer),
|
|
@@ -170,7 +171,7 @@ export const configure = (input = {}) => {
|
|
|
170
171
|
headers: Headers.fromInput({ ...input.headers, ...req.http?.headers }),
|
|
171
172
|
});
|
|
172
173
|
const res = yield* send(HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.bodyText(body, "application/json")));
|
|
173
|
-
const http =
|
|
174
|
+
const http = RequestExecutor.responseHttp(res);
|
|
174
175
|
const fail = (message, cause, body) => new AIError({
|
|
175
176
|
reason: new InvalidProviderOutputError({
|
|
176
177
|
route: "vercel-evaluation",
|
package/dist/route/client.js
CHANGED
|
@@ -15,7 +15,7 @@ const makeRouteLanguageModel = (route, mapped) => {
|
|
|
15
15
|
const provider = route.provider ?? ("provider" in mapped ? mapped.provider : undefined);
|
|
16
16
|
if (!provider)
|
|
17
17
|
throw new Error(`Route.model(${route.id}) requires a provider`);
|
|
18
|
-
if (!
|
|
18
|
+
if (!route.endpoint.baseURL)
|
|
19
19
|
throw new ProviderConfigurationError({
|
|
20
20
|
provider: ProviderID.make(provider),
|
|
21
21
|
message: `Route.model(${route.id}) requires an endpoint baseURL — configure it on the route first`,
|
|
@@ -37,7 +37,6 @@ const mergeRouteDefaults = (base, patch) => {
|
|
|
37
37
|
http: mergeHttpOptions(base?.http, HttpOptions.make(patch.http), headers === undefined ? undefined : new HttpOptions({ headers })),
|
|
38
38
|
};
|
|
39
39
|
};
|
|
40
|
-
const endpointBaseURL = (endpoint) => typeof endpoint.baseURL === "string" ? endpoint.baseURL : undefined;
|
|
41
40
|
const mergeHeaders = (...items) => {
|
|
42
41
|
const entries = items.flatMap((item) => item === undefined ? [] : Object.entries(item).filter((entry) => entry[1] !== undefined));
|
|
43
42
|
if (entries.length === 0)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Context, type Effect } from "effect";
|
|
2
|
-
import type { HttpClientRequest, HttpClientResponse } from "effect/http";
|
|
3
|
-
import type
|
|
2
|
+
import type { Headers, HttpClientRequest, HttpClientResponse } from "effect/http";
|
|
3
|
+
import { HttpContext, type AIError } from "../schema/errors.js";
|
|
4
4
|
export interface Interface {
|
|
5
5
|
readonly execute: (request: HttpClientRequest.HttpClientRequest, middleware?: HttpMiddleware) => Effect.Effect<HttpClientResponse.HttpClientResponse, AIError>;
|
|
6
6
|
}
|
|
@@ -11,4 +11,8 @@ export declare class RequestExecutorService extends RequestExecutorService_base
|
|
|
11
11
|
}
|
|
12
12
|
export declare const Service: typeof RequestExecutorService;
|
|
13
13
|
export type Service = RequestExecutorService;
|
|
14
|
+
export declare const headerDetails: (headers: Headers.Headers) => {
|
|
15
|
+
[k: string]: string;
|
|
16
|
+
};
|
|
17
|
+
export declare const responseHttp: (response: HttpClientResponse.HttpClientResponse) => HttpContext;
|
|
14
18
|
export {};
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { Context } from "effect";
|
|
2
|
+
import { HttpContext } from "../schema/errors.js";
|
|
2
3
|
export class RequestExecutorService extends Context.Service()("@opencode/AI/RequestExecutor") {
|
|
3
4
|
}
|
|
4
5
|
export const Service = RequestExecutorService;
|
|
6
|
+
export const headerDetails = (headers) => Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, String(value)]));
|
|
7
|
+
export const responseHttp = (response) => new HttpContext({
|
|
8
|
+
url: response.request.url,
|
|
9
|
+
status: response.status,
|
|
10
|
+
headers: headerDetails(response.headers),
|
|
11
|
+
});
|
package/dist/route/executor.d.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { Layer, Stream } from "effect";
|
|
2
2
|
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http";
|
|
3
|
-
import {
|
|
3
|
+
import { AIError } from "../schema/index.js";
|
|
4
4
|
import { Service, type HttpMiddleware, type Interface } from "./executor-service.js";
|
|
5
|
-
export { Service } from "./executor-service.js";
|
|
5
|
+
export { responseHttp, Service } from "./executor-service.js";
|
|
6
6
|
export type { HttpHandler, HttpMiddleware, Interface } from "./executor-service.js";
|
|
7
|
-
export declare const responseHttp: (response: HttpClientResponse.HttpClientResponse) => HttpContext;
|
|
8
7
|
/** Preserve HTTP diagnostics for executor and externally captured failures alike. */
|
|
9
8
|
export declare const httpFailure: (input: {
|
|
10
9
|
readonly message: string;
|
package/dist/route/executor.js
CHANGED
|
@@ -2,23 +2,23 @@ import { Cause, Effect, Layer, Option, Schema, Stream } from "effect";
|
|
|
2
2
|
import { FetchHttpClient, Headers, HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse, } from "effect/http";
|
|
3
3
|
import { HttpContext, HttpRateLimitDetails, AIError, TransportError } from "../schema/index.js";
|
|
4
4
|
import { classifyProviderFailure, providerErrorMessage } from "../provider-error.js";
|
|
5
|
-
import { Service } from "./executor-service.js";
|
|
6
|
-
export { Service } from "./executor-service.js";
|
|
7
|
-
const headerDetails = (headers) => Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, String(value)]));
|
|
5
|
+
import { headerDetails, responseHttp, Service } from "./executor-service.js";
|
|
6
|
+
export { responseHttp, Service } from "./executor-service.js";
|
|
8
7
|
const normalizedHeaders = (headers) => Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]));
|
|
8
|
+
const clampRetryAfterMs = (millis) => Math.min(Number.MAX_SAFE_INTEGER, Math.max(0, Math.ceil(millis)));
|
|
9
9
|
const retryAfterMs = (headers) => {
|
|
10
10
|
const millis = Number(headers["retry-after-ms"]);
|
|
11
11
|
if (Number.isFinite(millis))
|
|
12
|
-
return
|
|
12
|
+
return clampRetryAfterMs(millis);
|
|
13
13
|
const value = headers["retry-after"];
|
|
14
14
|
if (!value)
|
|
15
15
|
return undefined;
|
|
16
16
|
const seconds = Number(value);
|
|
17
17
|
if (Number.isFinite(seconds))
|
|
18
|
-
return
|
|
18
|
+
return clampRetryAfterMs(seconds * 1000);
|
|
19
19
|
const date = Date.parse(value);
|
|
20
20
|
if (!Number.isNaN(date))
|
|
21
|
-
return
|
|
21
|
+
return clampRetryAfterMs(date - Date.now());
|
|
22
22
|
return undefined;
|
|
23
23
|
};
|
|
24
24
|
const addRateLimitValue = (target, key, value) => {
|
|
@@ -60,11 +60,6 @@ const rateLimitDetails = (headers, retryAfter) => {
|
|
|
60
60
|
reset: Object.keys(reset).length === 0 ? undefined : reset,
|
|
61
61
|
});
|
|
62
62
|
};
|
|
63
|
-
export const responseHttp = (response) => new HttpContext({
|
|
64
|
-
url: response.request.url,
|
|
65
|
-
status: response.status,
|
|
66
|
-
headers: headerDetails(response.headers),
|
|
67
|
-
});
|
|
68
63
|
const MAX_BODY_CHARS = 2000;
|
|
69
64
|
// Without a recognized message, show the raw body so the provider's explanation is never dropped.
|
|
70
65
|
const providerMessage = (status, body) => {
|
|
@@ -3,7 +3,7 @@ import { HttpClientResponse } from "effect/http";
|
|
|
3
3
|
import type { Snapshot, Status } from "../generation.js";
|
|
4
4
|
import { Media } from "../media.js";
|
|
5
5
|
import type { AuthInput } from "./auth.js";
|
|
6
|
-
import { AIError, AuthenticationError, HttpContext, InvalidRequestError, ProviderID, ProviderInternalError, RateLimitError } from "../schema/index.js";
|
|
6
|
+
import { AIError, AuthenticationError, type HttpContext, InvalidRequestError, ProviderID, ProviderInternalError, RateLimitError } from "../schema/index.js";
|
|
7
7
|
/** Array values become repeated parameters (`keyterm=a&keyterm=b`). */
|
|
8
8
|
export type Query = Readonly<Record<string, string | ReadonlyArray<string>>>;
|
|
9
9
|
/** `query` is appended to the endpoint URL before the route and caller `http.query` overlays. */
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Clock, Duration, Effect, Schema } from "effect";
|
|
2
2
|
import { HttpClientResponse } from "effect/http";
|
|
3
3
|
import { Media } from "../media.js";
|
|
4
|
-
import {
|
|
4
|
+
import { RequestExecutor } from "./executor.js";
|
|
5
|
+
import { AIError, AuthenticationError, ContentPolicyError, InvalidProviderOutputError, InvalidRequestError, ProviderID, ProviderInternalError, RateLimitError, UnsupportedOperationError, } from "../schema/index.js";
|
|
5
6
|
export const json = (value, query) => ({
|
|
6
7
|
type: "json",
|
|
7
8
|
value,
|
|
@@ -27,7 +28,6 @@ const FAILURES = {
|
|
|
27
28
|
RateLimit: RateLimitError,
|
|
28
29
|
ProviderInternal: ProviderInternalError,
|
|
29
30
|
};
|
|
30
|
-
const context = (response) => new HttpContext({ url: response.request.url, status: response.status, headers: response.headers });
|
|
31
31
|
/** One protocol's route id, display name, and provider, with the decoders and errors that carry them. */
|
|
32
32
|
export const identity = (input) => {
|
|
33
33
|
const provider = ProviderID.make(input.provider);
|
|
@@ -40,7 +40,7 @@ export const identity = (input) => {
|
|
|
40
40
|
* `contentPolicy` is a moderated result.
|
|
41
41
|
*/
|
|
42
42
|
const text = Effect.fn("MediaProtocol.text")(function* (response) {
|
|
43
|
-
const http =
|
|
43
|
+
const http = RequestExecutor.responseHttp(response);
|
|
44
44
|
const body = yield* response.text.pipe(Effect.mapError((cause) => new AIError({
|
|
45
45
|
reason: new InvalidProviderOutputError({
|
|
46
46
|
route: input.id,
|
|
@@ -2,7 +2,6 @@ import { Effect, Stream } from "effect";
|
|
|
2
2
|
import { Headers } from "effect/http";
|
|
3
3
|
import { Socket } from "effect/socket";
|
|
4
4
|
import { AIError, type HttpContext } from "../../schema/index.js";
|
|
5
|
-
import type { Transport } from "./index.js";
|
|
6
5
|
import type { WebSocketChannelExecutor } from "./websocket-channel.js";
|
|
7
6
|
export interface WebSocketRequest {
|
|
8
7
|
readonly url: string;
|
|
@@ -23,30 +22,7 @@ export declare const fromWebSocket: (ws: Socket.WebSocketLike, input: WebSocketR
|
|
|
23
22
|
export declare const messageText: (message: string | Uint8Array, decoder: TextDecoder) => string;
|
|
24
23
|
export declare const makeDirect: (connector: WebSocketConnector) => WebSocketChannelExecutor;
|
|
25
24
|
export declare const direct: Effect.Effect<WebSocketChannelExecutor, never, Socket.WebSocketConstructor>;
|
|
26
|
-
export interface JsonPrepared {
|
|
27
|
-
readonly url: string;
|
|
28
|
-
readonly headers: Headers.Headers;
|
|
29
|
-
readonly message: string;
|
|
30
|
-
}
|
|
31
|
-
export interface JsonInput<Body, Message> {
|
|
32
|
-
readonly toMessage: (body: Body | Record<string, unknown>) => Effect.Effect<Message, AIError>;
|
|
33
|
-
readonly encodeMessage: (message: Message) => string;
|
|
34
|
-
}
|
|
35
|
-
export type JsonPatch<Body, Message> = Partial<JsonInput<Body, Message>>;
|
|
36
|
-
export interface JsonTransport<Body, Message> extends Transport<Body, JsonPrepared, string> {
|
|
37
|
-
readonly with: (patch: JsonPatch<Body, Message>) => JsonTransport<Body, Message>;
|
|
38
|
-
}
|
|
39
|
-
export declare const json: <Body, Message>(input: JsonInput<Body, Message>) => JsonTransport<Body, Message>;
|
|
40
|
-
export declare const jsonTransport: {
|
|
41
|
-
readonly id: "websocket-json";
|
|
42
|
-
readonly with: typeof json;
|
|
43
|
-
};
|
|
44
25
|
export declare const WebSocketTransport: {
|
|
45
|
-
readonly json: typeof json;
|
|
46
|
-
readonly jsonTransport: {
|
|
47
|
-
readonly id: "websocket-json";
|
|
48
|
-
readonly with: typeof json;
|
|
49
|
-
};
|
|
50
26
|
readonly direct: Effect.Effect<WebSocketChannelExecutor, never, Socket.WebSocketConstructor>;
|
|
51
27
|
readonly makeDirect: typeof makeDirect;
|
|
52
28
|
readonly open: typeof open;
|
|
@@ -2,7 +2,6 @@ import { Cause, Effect, Queue, Stream } from "effect";
|
|
|
2
2
|
import { Headers } from "effect/http";
|
|
3
3
|
import { Socket } from "effect/socket";
|
|
4
4
|
import { AIError, AIErrorReason, TransportError, } from "../../schema/index.js";
|
|
5
|
-
import * as HttpTransport from "./http.js";
|
|
6
5
|
const MAX_FRAME_BYTES = 16 * 1024 * 1024;
|
|
7
6
|
const transportError = (message, input) => new AIError({
|
|
8
7
|
reason: new TransportError({
|
|
@@ -300,56 +299,7 @@ export const direct = Effect.gen(function* () {
|
|
|
300
299
|
open: (input) => open(input).pipe(Effect.provideService(Socket.WebSocketConstructor, constructor)),
|
|
301
300
|
});
|
|
302
301
|
});
|
|
303
|
-
export const json = (input) => ({
|
|
304
|
-
id: "websocket-json",
|
|
305
|
-
with: (patch) => json({ ...input, ...patch }),
|
|
306
|
-
prepare: (prepareInput) => Effect.gen(function* () {
|
|
307
|
-
const parts = yield* HttpTransport.jsonRequestParts({
|
|
308
|
-
...prepareInput,
|
|
309
|
-
});
|
|
310
|
-
return {
|
|
311
|
-
url: yield* toWebSocketUrl(parts.url),
|
|
312
|
-
headers: parts.headers,
|
|
313
|
-
message: input.encodeMessage(yield* input.toMessage(parts.jsonBody)),
|
|
314
|
-
};
|
|
315
|
-
}),
|
|
316
|
-
execute: (prepared, request, _runtime, options) => {
|
|
317
|
-
const webSocket = options?.webSocket;
|
|
318
|
-
if (!webSocket) {
|
|
319
|
-
return Effect.fail(transportError("WebSocket JSON transport requires StreamOptions.webSocket", {
|
|
320
|
-
url: prepared.url,
|
|
321
|
-
operation: "request",
|
|
322
|
-
code: "unavailable",
|
|
323
|
-
phase: "prepare",
|
|
324
|
-
delivery: "not-sent",
|
|
325
|
-
}));
|
|
326
|
-
}
|
|
327
|
-
const driver = {
|
|
328
|
-
create: () => Effect.succeed({ message: prepared.message, mode: "full" }),
|
|
329
|
-
observe: (_create, frame) => Effect.succeed({ type: "frame", frame }),
|
|
330
|
-
};
|
|
331
|
-
const exchange = {
|
|
332
|
-
id: request.id ?? "request",
|
|
333
|
-
connect: { url: prepared.url, headers: prepared.headers },
|
|
334
|
-
fallback: () => Stream.fail(transportError("WebSocket JSON transport does not provide HTTP fallback", {
|
|
335
|
-
url: prepared.url,
|
|
336
|
-
operation: "request",
|
|
337
|
-
code: "websocket",
|
|
338
|
-
phase: "fallback",
|
|
339
|
-
delivery: "not-sent",
|
|
340
|
-
})),
|
|
341
|
-
driver,
|
|
342
|
-
};
|
|
343
|
-
return webSocket.execute(exchange);
|
|
344
|
-
},
|
|
345
|
-
});
|
|
346
|
-
export const jsonTransport = {
|
|
347
|
-
id: "websocket-json",
|
|
348
|
-
with: json,
|
|
349
|
-
};
|
|
350
302
|
export const WebSocketTransport = {
|
|
351
|
-
json,
|
|
352
|
-
jsonTransport,
|
|
353
303
|
direct,
|
|
354
304
|
makeDirect,
|
|
355
305
|
open,
|
package/dist/schema/errors.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ declare const HttpContext_base: Schema.Class<HttpContext, Schema.Struct<{
|
|
|
10
10
|
export declare class HttpContext extends HttpContext_base {
|
|
11
11
|
}
|
|
12
12
|
declare const HttpRateLimitDetails_base: Schema.Class<HttpRateLimitDetails, Schema.Struct<{
|
|
13
|
-
readonly retryAfterMs: Schema.optional<Schema.
|
|
13
|
+
readonly retryAfterMs: Schema.optional<Schema.Int>;
|
|
14
14
|
readonly limit: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
|
|
15
15
|
readonly remaining: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
|
|
16
16
|
readonly reset: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
|
|
@@ -80,16 +80,17 @@ declare const RateLimitError_base: Schema.Class<RateLimitError, Schema.TaggedStr
|
|
|
80
80
|
readonly body: Schema.optional<Schema.String>;
|
|
81
81
|
readonly http: Schema.optional<typeof HttpContext>;
|
|
82
82
|
readonly cause: Schema.optional<Schema.Defect>;
|
|
83
|
-
readonly retryAfterMs: Schema.optional<Schema.
|
|
83
|
+
readonly retryAfterMs: Schema.optional<Schema.Int>;
|
|
84
84
|
readonly rateLimit: Schema.optional<typeof HttpRateLimitDetails>;
|
|
85
85
|
}>, import("effect/Cause").YieldableError>;
|
|
86
86
|
export declare class RateLimitError extends RateLimitError_base {
|
|
87
87
|
}
|
|
88
88
|
declare const QuotaExceededError_base: Schema.Class<QuotaExceededError, Schema.TaggedStruct<"QuotaExceeded", {
|
|
89
|
-
message: Schema.String;
|
|
90
|
-
body: Schema.optional<Schema.String>;
|
|
91
|
-
http: Schema.optional<typeof HttpContext>;
|
|
92
|
-
cause: Schema.optional<Schema.Defect>;
|
|
89
|
+
readonly message: Schema.String;
|
|
90
|
+
readonly body: Schema.optional<Schema.String>;
|
|
91
|
+
readonly http: Schema.optional<typeof HttpContext>;
|
|
92
|
+
readonly cause: Schema.optional<Schema.Defect>;
|
|
93
|
+
readonly retryAfterMs: Schema.optional<Schema.Int>;
|
|
93
94
|
}>, import("effect/Cause").YieldableError>;
|
|
94
95
|
export declare class QuotaExceededError extends QuotaExceededError_base {
|
|
95
96
|
}
|
|
@@ -106,7 +107,7 @@ declare const ProviderInternalError_base: Schema.Class<ProviderInternalError, Sc
|
|
|
106
107
|
readonly body: Schema.optional<Schema.String>;
|
|
107
108
|
readonly http: Schema.optional<typeof HttpContext>;
|
|
108
109
|
readonly cause: Schema.optional<Schema.Defect>;
|
|
109
|
-
readonly retryAfterMs: Schema.optional<Schema.
|
|
110
|
+
readonly retryAfterMs: Schema.optional<Schema.Int>;
|
|
110
111
|
}>, import("effect/Cause").YieldableError>;
|
|
111
112
|
export declare class ProviderInternalError extends ProviderInternalError_base {
|
|
112
113
|
}
|
package/dist/schema/errors.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
|
+
import { NonNegativeInt } from "@opencode/schema";
|
|
2
3
|
import { Tool } from "@opencode/schema/tool";
|
|
3
4
|
import { ModelID, ProviderID, RouteID } from "./ids.js";
|
|
4
5
|
export const ProviderFailureClassification = Schema.Literals(["context-overflow", "payload-too-large"]);
|
|
@@ -9,7 +10,7 @@ export class HttpContext extends Schema.Class("AI.HttpContext")({
|
|
|
9
10
|
}) {
|
|
10
11
|
}
|
|
11
12
|
export class HttpRateLimitDetails extends Schema.Class("AI.HttpRateLimitDetails")({
|
|
12
|
-
retryAfterMs: Schema.optional(
|
|
13
|
+
retryAfterMs: Schema.optional(NonNegativeInt),
|
|
13
14
|
limit: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
|
14
15
|
remaining: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
|
15
16
|
reset: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
|
@@ -63,17 +64,20 @@ export class AuthenticationError extends Schema.TaggedError("AI.Error.Authentica
|
|
|
63
64
|
}
|
|
64
65
|
export class RateLimitError extends Schema.TaggedError("AI.Error.RateLimit")("RateLimit", {
|
|
65
66
|
...ReasonFields,
|
|
66
|
-
retryAfterMs: Schema.optional(
|
|
67
|
+
retryAfterMs: Schema.optional(NonNegativeInt),
|
|
67
68
|
rateLimit: Schema.optional(HttpRateLimitDetails),
|
|
68
69
|
}) {
|
|
69
70
|
}
|
|
70
|
-
export class QuotaExceededError extends Schema.TaggedError("AI.Error.QuotaExceeded")("QuotaExceeded",
|
|
71
|
+
export class QuotaExceededError extends Schema.TaggedError("AI.Error.QuotaExceeded")("QuotaExceeded", {
|
|
72
|
+
...ReasonFields,
|
|
73
|
+
retryAfterMs: Schema.optional(NonNegativeInt),
|
|
74
|
+
}) {
|
|
71
75
|
}
|
|
72
76
|
export class ContentPolicyError extends Schema.TaggedError("AI.Error.ContentPolicy")("ContentPolicy", ReasonFields) {
|
|
73
77
|
}
|
|
74
78
|
export class ProviderInternalError extends Schema.TaggedError("AI.Error.ProviderInternal")("ProviderInternal", {
|
|
75
79
|
...ReasonFields,
|
|
76
|
-
retryAfterMs: Schema.optional(
|
|
80
|
+
retryAfterMs: Schema.optional(NonNegativeInt),
|
|
77
81
|
}) {
|
|
78
82
|
}
|
|
79
83
|
export const TransportType = Schema.Literals(["http", "websocket"]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
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.1.
|
|
37
|
+
"@opencode/http-recorder": "2.1.2",
|
|
38
38
|
"@tsconfig/bun": "1.0.9",
|
|
39
39
|
"@types/bun": "1.4.0",
|
|
40
40
|
"@typescript/native": "npm:typescript@7.0.2",
|
|
@@ -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.1.
|
|
47
|
+
"@opencode/schema": "2.1.2",
|
|
48
48
|
"aws4fetch": "1.0.20",
|
|
49
49
|
"effect": "4.0.1",
|
|
50
50
|
"google-auth-library": "10.5.0"
|