@opencode/ai 2.0.31 → 2.0.32
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.
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Effect, type Redacted } from "effect";
|
|
2
|
+
import { AIError } from "../schema/index.js";
|
|
3
|
+
export type RealtimeTransportInput = {
|
|
4
|
+
readonly type: "webrtc";
|
|
5
|
+
readonly sdp: string;
|
|
6
|
+
readonly dcid?: number;
|
|
7
|
+
} | {
|
|
8
|
+
readonly type: "websocket";
|
|
9
|
+
};
|
|
10
|
+
export type RealtimeTransport = {
|
|
11
|
+
readonly type: "webrtc";
|
|
12
|
+
readonly sdp: string;
|
|
13
|
+
} | {
|
|
14
|
+
readonly type: "websocket";
|
|
15
|
+
readonly url: string;
|
|
16
|
+
readonly token: string;
|
|
17
|
+
readonly expiresAt: number;
|
|
18
|
+
};
|
|
19
|
+
export type RealtimeSession = Readonly<Record<string, unknown>>;
|
|
20
|
+
export interface RealtimeConnectInput {
|
|
21
|
+
readonly model: string;
|
|
22
|
+
readonly apiKey: Redacted.Redacted;
|
|
23
|
+
readonly session?: RealtimeSession;
|
|
24
|
+
readonly transport: RealtimeTransportInput;
|
|
25
|
+
}
|
|
26
|
+
export declare const engine: (model: string) => {
|
|
27
|
+
readonly family: "gpt-live";
|
|
28
|
+
readonly id: "openai-live";
|
|
29
|
+
readonly provider: "openai";
|
|
30
|
+
} | {
|
|
31
|
+
readonly family: "gpt-realtime";
|
|
32
|
+
readonly id: "openai-realtime";
|
|
33
|
+
readonly provider: "openai";
|
|
34
|
+
} | {
|
|
35
|
+
readonly family: "grok-voice";
|
|
36
|
+
readonly id: "xai-realtime";
|
|
37
|
+
readonly provider: "xai";
|
|
38
|
+
} | undefined;
|
|
39
|
+
export declare const connect: (input: RealtimeConnectInput) => Effect.Effect<RealtimeTransport, AIError, import("../route/executor-service.js").RequestExecutorService>;
|
|
40
|
+
export declare const Realtime: {
|
|
41
|
+
readonly engine: (model: string) => {
|
|
42
|
+
readonly family: "gpt-live";
|
|
43
|
+
readonly id: "openai-live";
|
|
44
|
+
readonly provider: "openai";
|
|
45
|
+
} | {
|
|
46
|
+
readonly family: "gpt-realtime";
|
|
47
|
+
readonly id: "openai-realtime";
|
|
48
|
+
readonly provider: "openai";
|
|
49
|
+
} | {
|
|
50
|
+
readonly family: "grok-voice";
|
|
51
|
+
readonly id: "xai-realtime";
|
|
52
|
+
readonly provider: "xai";
|
|
53
|
+
} | undefined;
|
|
54
|
+
readonly connect: (input: RealtimeConnectInput) => Effect.Effect<RealtimeTransport, AIError, import("../route/executor-service.js").RequestExecutorService>;
|
|
55
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { HttpClientRequest } from "effect/http";
|
|
3
|
+
import { RequestExecutor } from "../route/executor.js";
|
|
4
|
+
import { AIError, HttpContext, InvalidProviderOutputError, InvalidRequestError } from "../schema/index.js";
|
|
5
|
+
import { encodeJson } from "../utils/json.js";
|
|
6
|
+
const OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
7
|
+
const XAI_BASE_URL = "https://api.x.ai/v1";
|
|
8
|
+
const engines = [
|
|
9
|
+
{ family: "gpt-live", id: "openai-live", provider: "openai" },
|
|
10
|
+
{ family: "gpt-realtime", id: "openai-realtime", provider: "openai" },
|
|
11
|
+
{ family: "grok-voice", id: "xai-realtime", provider: "xai" },
|
|
12
|
+
];
|
|
13
|
+
// Matches the bare family name (`gpt-realtime`) and its versions (`gpt-realtime-2.1`, `gpt-realtime-mini`).
|
|
14
|
+
export const engine = (model) => engines.find((item) => model === item.family || model.startsWith(`${item.family}-`));
|
|
15
|
+
export const connect = Effect.fn("Realtime.connect")(function* (input) {
|
|
16
|
+
const resolved = engine(input.model);
|
|
17
|
+
if (!resolved)
|
|
18
|
+
return yield* invalidRequest(`Unsupported voice model: ${input.model}`);
|
|
19
|
+
const session = { ...input.session, model: input.model };
|
|
20
|
+
switch (resolved.id) {
|
|
21
|
+
case "openai-live": {
|
|
22
|
+
if (input.transport.type !== "webrtc")
|
|
23
|
+
return yield* invalidRequest("OpenAI GPT-Live only supports WebRTC transport");
|
|
24
|
+
const data = yield* post(resolved.id, `${OPENAI_BASE_URL}/live/sessions`, input.apiKey, { session, transport: input.transport }, LiveSessionResponse);
|
|
25
|
+
return { type: "webrtc", sdp: data.transport.sdp };
|
|
26
|
+
}
|
|
27
|
+
case "openai-realtime": {
|
|
28
|
+
if (input.transport.type === "websocket")
|
|
29
|
+
return yield* clientSecret(resolved.id, OPENAI_BASE_URL, input.model, input.apiKey, session);
|
|
30
|
+
const form = new FormData();
|
|
31
|
+
form.append("sdp", input.transport.sdp);
|
|
32
|
+
form.append("session", encodeJson(session));
|
|
33
|
+
const query = input.transport.dcid === undefined ? "" : `?dcid=${input.transport.dcid}`;
|
|
34
|
+
const sdp = yield* post(resolved.id, `${OPENAI_BASE_URL}/realtime/calls${query}`, input.apiKey, form, Schema.String);
|
|
35
|
+
return { type: "webrtc", sdp };
|
|
36
|
+
}
|
|
37
|
+
case "xai-realtime": {
|
|
38
|
+
if (input.transport.type !== "websocket")
|
|
39
|
+
return yield* invalidRequest("xAI Grok Voice only supports WebSocket transport");
|
|
40
|
+
return yield* clientSecret(resolved.id, XAI_BASE_URL, input.model, input.apiKey);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
export const Realtime = {
|
|
45
|
+
engine,
|
|
46
|
+
connect,
|
|
47
|
+
};
|
|
48
|
+
const LiveSessionResponse = Schema.fromJsonString(Schema.Struct({
|
|
49
|
+
transport: Schema.Struct({ sdp: Schema.String }),
|
|
50
|
+
}));
|
|
51
|
+
// xAI's ephemeral-token docs (https://docs.x.ai/developers/model-capabilities/audio/ephemeral-tokens)
|
|
52
|
+
// document the request body (`{ expires_after: { seconds } }`) but not the response payload; we assume
|
|
53
|
+
// it mirrors OpenAI's `{ value, expires_at }` response shape.
|
|
54
|
+
const ClientSecretResponse = Schema.fromJsonString(Schema.Struct({
|
|
55
|
+
value: Schema.String,
|
|
56
|
+
expires_at: Schema.Number,
|
|
57
|
+
}));
|
|
58
|
+
const invalidRequest = (message) => new AIError({ reason: new InvalidRequestError({ message }) });
|
|
59
|
+
const post = (route, url, apiKey, body, response) => Effect.gen(function* () {
|
|
60
|
+
const executor = yield* RequestExecutor.Service;
|
|
61
|
+
const res = yield* executor.execute(HttpClientRequest.post(url).pipe(HttpClientRequest.bearerToken(apiKey), body instanceof FormData
|
|
62
|
+
? HttpClientRequest.bodyFormData(body)
|
|
63
|
+
: HttpClientRequest.bodyText(encodeJson(body), "application/json")));
|
|
64
|
+
const http = new HttpContext({ url: res.request.url, status: res.status, headers: res.headers });
|
|
65
|
+
const fail = (message, cause, raw) => new AIError({ reason: new InvalidProviderOutputError({ route, message, body: raw, http, cause }) });
|
|
66
|
+
const text = yield* res.text.pipe(Effect.mapError((cause) => fail(`Failed to read ${route} response`, cause)));
|
|
67
|
+
return yield* Schema.decodeEffect(response)(text).pipe(Effect.mapError((cause) => fail(`${route} returned an invalid response`, cause, text)));
|
|
68
|
+
});
|
|
69
|
+
const clientSecret = (route, baseURL, model, apiKey, session) => post(route, `${baseURL}/realtime/client_secrets`, apiKey, { expires_after: { seconds: 60 }, session }, ClientSecretResponse).pipe(Effect.map((data) => ({
|
|
70
|
+
type: "websocket",
|
|
71
|
+
url: `${baseURL.replace(/^http/, "ws")}/realtime?model=${encodeURIComponent(model)}`,
|
|
72
|
+
token: data.value,
|
|
73
|
+
expiresAt: data.expires_at,
|
|
74
|
+
})));
|
package/dist/experimental.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { EvaluationClient } from "./experimental/evaluation-client.js";
|
|
2
2
|
export { BooleanAnswer, BooleanQuestion, ChoiceAnswer, ChoiceQuestion, Evaluation, EvaluationAnswer, EvaluationInput, EvaluationModel, EvaluationModelSchema, EvaluationQuestion, EvaluationRequest, EvaluationResponse, EvaluationRounding, ScoreAnswer, ScoreQuestion, } from "./experimental/evaluation.js";
|
|
3
3
|
export type { AnswerFor, AnswersFor, EvaluationModelOptions, EvaluationOptions, EvaluationQuestions, EvaluationRequestFor, EvaluationRequestInput, EvaluationResponseFor, EvaluationRoute, } from "./experimental/evaluation.js";
|
|
4
|
+
export { Realtime } from "./experimental/realtime.js";
|
|
5
|
+
export type { RealtimeConnectInput, RealtimeSession, RealtimeTransport, RealtimeTransportInput, } from "./experimental/realtime.js";
|
package/dist/experimental.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { EvaluationClient } from "./experimental/evaluation-client.js";
|
|
2
2
|
export { BooleanAnswer, BooleanQuestion, ChoiceAnswer, ChoiceQuestion, Evaluation, EvaluationAnswer, EvaluationInput, EvaluationModel, EvaluationModelSchema, EvaluationQuestion, EvaluationRequest, EvaluationResponse, EvaluationRounding, ScoreAnswer, ScoreQuestion, } from "./experimental/evaluation.js";
|
|
3
|
+
export { Realtime } from "./experimental/realtime.js";
|
package/dist/protocols/gemini.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Auth } from "../route/auth.js";
|
|
|
5
5
|
import { Endpoint } from "../route/endpoint.js";
|
|
6
6
|
import { Protocol } from "../route/protocol.js";
|
|
7
7
|
import { HttpTransport, } from "../route/transport/index.js";
|
|
8
|
-
import { AIError, LLMEvent, Usage, } from "../schema/index.js";
|
|
8
|
+
import { AIError, InvalidProviderOutputError, LLMEvent, Usage, } from "../schema/index.js";
|
|
9
9
|
import { classifyProviderFailure } from "../provider-error.js";
|
|
10
10
|
import { Media } from "../media.js";
|
|
11
11
|
import { JsonObject, knownString, lenient, optionalArray, optionalNull, ProviderShared } from "./shared.js";
|
|
@@ -534,6 +534,16 @@ const step = (state, event) => {
|
|
|
534
534
|
usage: event.usageMetadata ?? state.usage,
|
|
535
535
|
finishReason: candidate?.finishReason ?? state.finishReason,
|
|
536
536
|
};
|
|
537
|
+
// Gemini sporadically ends a valid request with MALFORMED_RESPONSE; continuing or resending it recovers.
|
|
538
|
+
if (candidate?.finishReason === "MALFORMED_RESPONSE")
|
|
539
|
+
return Effect.fail(new AIError({
|
|
540
|
+
reason: new InvalidProviderOutputError({
|
|
541
|
+
route: state.route,
|
|
542
|
+
message: `Gemini stopped with ${candidate.finishReason}`,
|
|
543
|
+
body: ProviderShared.encodeJson(event),
|
|
544
|
+
classification: "incomplete-stream",
|
|
545
|
+
}),
|
|
546
|
+
}));
|
|
537
547
|
if (candidate?.finishReason && mapFinishReason(candidate.finishReason, state.hasToolCalls) === "error")
|
|
538
548
|
return Effect.fail(ProviderShared.eventError(state.route, `Gemini stopped with ${candidate.finishReason}`, ProviderShared.encodeJson(event)));
|
|
539
549
|
if (!candidate?.content)
|
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.32",
|
|
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.32",
|
|
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.32",
|
|
48
48
|
"aws4fetch": "1.0.20",
|
|
49
49
|
"effect": "4.0.1",
|
|
50
50
|
"google-auth-library": "10.5.0"
|