@xandreed/plugin-models 0.12.0 → 0.14.0
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/auth/anthropicOAuth.d.ts +16 -2
- package/dist/auth/anthropicOAuth.js +14 -11
- package/dist/auth/credential-input.entity.d.ts +34 -0
- package/dist/auth/credential-input.entity.functions.d.ts +8 -0
- package/dist/auth/credential-input.entity.functions.js +9 -0
- package/dist/auth/credential-input.entity.js +20 -0
- package/dist/auth/credential-refresh.entity.d.ts +9 -0
- package/dist/auth/credential-refresh.entity.functions.d.ts +15 -0
- package/dist/auth/credential-refresh.entity.functions.js +12 -0
- package/dist/auth/credential-refresh.entity.functions.test.d.ts +1 -0
- package/dist/auth/credential-refresh.entity.js +2 -0
- package/dist/auth/localAuth.d.ts +2 -1
- package/dist/auth/localAuth.js +41 -43
- package/dist/auth/memory-auth.adapter.d.ts +27 -0
- package/dist/auth/memory-auth.adapter.js +47 -0
- package/dist/auth/memory-auth.adapter.test.d.ts +1 -0
- package/dist/auth/openAiCodexOAuth.d.ts +3 -2
- package/dist/auth/openAiCodexOAuth.js +9 -8
- package/dist/index.d.ts +23 -6
- package/dist/index.js +28 -1
- package/dist/llm/compat.d.ts +39 -8
- package/dist/llm/compat.js +80 -29
- package/dist/llm/modelCatalog.d.ts +20 -1
- package/dist/llm/modelCatalog.js +19 -7
- package/dist/llm/openAiCodex.d.ts +4 -1
- package/dist/llm/openAiCodex.js +2 -2
- package/dist/llm/openAiCodexWebSocket.d.ts +23 -3
- package/dist/llm/openAiCodexWebSocket.js +125 -71
- package/dist/llm/openAiCodexWebSocket.test.d.ts +1 -0
- package/dist/llm/providers.d.ts +18 -4
- package/dist/llm/providers.js +49 -24
- package/dist/llm/router.d.ts +11 -1
- package/dist/llm/router.js +55 -31
- package/dist/llm/sse.d.ts +25 -3
- package/dist/llm/sse.js +8 -8
- package/dist/llm/utilityLlm.d.ts +1 -1
- package/dist/llm/utilityLlm.js +4 -4
- package/dist/plugin.adapter.d.ts +64 -4
- package/dist/plugin.adapter.js +48 -10
- package/dist/ports/host-credentials.port.d.ts +21 -0
- package/dist/ports/host-credentials.port.js +4 -0
- package/dist/ports/model-endpoints.port.d.ts +16 -3
- package/dist/ports/model-endpoints.port.js +7 -3
- package/dist/ports/models-transport.port.d.ts +46 -0
- package/dist/ports/models-transport.port.js +4 -0
- package/dist/settings/localSettings.js +6 -46
- package/dist/settings/memory-settings.adapter.d.ts +10 -0
- package/dist/settings/memory-settings.adapter.js +11 -0
- package/dist/settings/memory-settings.adapter.test.d.ts +1 -0
- package/dist/settings/settings-value.entity.d.ts +7 -0
- package/dist/settings/settings-value.entity.functions.d.ts +29 -0
- package/dist/settings/settings-value.entity.functions.js +43 -0
- package/dist/settings/settings-value.entity.functions.test.d.ts +1 -0
- package/dist/settings/settings-value.entity.js +2 -0
- package/dist/transport/http-proxy.entity.d.ts +90 -0
- package/dist/transport/http-proxy.entity.functions.d.ts +51 -0
- package/dist/transport/http-proxy.entity.functions.js +112 -0
- package/dist/transport/http-proxy.entity.functions.test.d.ts +1 -0
- package/dist/transport/http-proxy.entity.js +42 -0
- package/dist/transport/models-transport.adapter.d.ts +48 -0
- package/dist/transport/models-transport.adapter.js +102 -0
- package/dist/transport/models-transport.adapter.test.d.ts +1 -0
- package/dist/transport/recording-proxy.testing.d.ts +33 -0
- package/dist/transport/undici-fetch.adapter.d.ts +13 -0
- package/dist/transport/undici-fetch.adapter.js +1 -0
- package/package.json +10 -9
package/dist/llm/compat.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { AiError, LanguageModel, Tool } from "effect/ai";
|
|
2
|
-
import type { Prompt } from "effect/ai";
|
|
1
|
+
import { AiError, LanguageModel, Prompt, Tool } from "effect/ai";
|
|
3
2
|
import { Effect } from "effect";
|
|
4
|
-
import type { CompletionUsage } from "./sse.js";
|
|
3
|
+
import type { CompletionUsage, UsageReading } from "./sse.js";
|
|
4
|
+
import type { ModelsFetch } from "../ports/models-transport.port.js";
|
|
5
5
|
/**
|
|
6
6
|
* A generic OpenAI-compatible `/chat/completions` `LanguageModel` over raw
|
|
7
7
|
* `fetch`. The official `@effect/ai-openai` client targets api.openai.com;
|
|
@@ -32,16 +32,47 @@ export interface CompatConfig {
|
|
|
32
32
|
* thinking defaults or call policy — for endpoints that reject the
|
|
33
33
|
* gateway extensions (Gemini's OpenAI-compatible API). */
|
|
34
34
|
readonly standardOnly?: boolean;
|
|
35
|
-
/**
|
|
36
|
-
|
|
35
|
+
/** How the endpoint's usage is read (default: every count, reasoning
|
|
36
|
+
* included; see `UsageReading`). */
|
|
37
|
+
readonly usage?: UsageReading;
|
|
38
|
+
/** The model reads images: send them as `image_url` parts (default false: label lines; see `toChatMessages`). */
|
|
39
|
+
readonly images?: boolean;
|
|
40
|
+
/** The fetch requests go through (the models plugin passes its `ModelsTransport`'s, which knows the proxy); defaults to the global fetch. */
|
|
41
|
+
readonly fetchImpl?: ModelsFetch;
|
|
37
42
|
/** Host-owned admission (e.g. reserve a worst-case cost) before every call. */
|
|
38
43
|
readonly beforeRequest?: (body: Readonly<Record<string, unknown>>) => Effect.Effect<void, AiError.AiError>;
|
|
39
44
|
/** Per-call accounting/resource boundary. Non-streaming responses are fully
|
|
40
45
|
* buffered inside request so interruption also aborts body consumption. */
|
|
41
46
|
readonly aroundRequest?: (body: Readonly<Record<string, unknown>>, request: Effect.Effect<Response, AiError.AiError>) => Effect.Effect<Response, AiError.AiError>;
|
|
42
47
|
}
|
|
43
|
-
/**
|
|
44
|
-
export
|
|
48
|
+
/** How `toChatMessages` writes a request. */
|
|
49
|
+
export interface ChatMessageOptions {
|
|
50
|
+
/** Send images as `image_url` parts (default false: label lines). */
|
|
51
|
+
readonly images?: boolean;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Decoded `Prompt` messages → chat-completions message objects. A
|
|
55
|
+
* text-only user message is its text, as before. Without `images`, a user
|
|
56
|
+
* message is one string: its text, then a label line per file part, and a
|
|
57
|
+
* tool result is its text with a label line per image. With `images`, a
|
|
58
|
+
* user message with images is a content array of its parts in order (an
|
|
59
|
+
* image as an `image_url` part with a `data:` URL), and a tool result is its
|
|
60
|
+
* text in the tool message, while its images, which a tool message cannot
|
|
61
|
+
* carry, follow all the batch's tool messages in one user message, each
|
|
62
|
+
* result's captioned `Images returned by <tool> (call <id>):`; a user
|
|
63
|
+
* message right after the batch is merged into it, so no two user messages
|
|
64
|
+
* follow each other, captioned `The user's message:` when it has images of
|
|
65
|
+
* its own.
|
|
66
|
+
*/
|
|
67
|
+
export declare const toChatMessages: (prompt: Prompt.Prompt, options?: ChatMessageOptions) => ReadonlyArray<Json>;
|
|
68
|
+
/**
|
|
69
|
+
* A prompt with every file part a label line: what a route that reads no
|
|
70
|
+
* images is sent (the models plugin rewrites a request so on such routes).
|
|
71
|
+
* A user message with file parts becomes one text part (its text, then a
|
|
72
|
+
* label line each); a tool result with model output becomes its text with a
|
|
73
|
+
* label line per file part, as its result. Messages without files are kept.
|
|
74
|
+
*/
|
|
75
|
+
export declare const promptWithImageLabels: (prompt: Prompt.Prompt) => Prompt.Prompt;
|
|
45
76
|
/** User-defined tools → chat-completions function declarations (empty when
|
|
46
77
|
* the request carries none — the caller omits the `tools` key entirely). */
|
|
47
78
|
export declare const toChatTools: (tools: ReadonlyArray<Tool.Any>) => ReadonlyArray<Json>;
|
|
@@ -76,6 +107,6 @@ interface ChatCompletion {
|
|
|
76
107
|
readonly usage?: CompletionUsage;
|
|
77
108
|
}
|
|
78
109
|
/** Parse one non-streaming completion into `@effect/ai` encoded parts. */
|
|
79
|
-
export declare const fromChatCompletion: (moduleName: string, body: ChatCompletion) => Effect.Effect<ReadonlyArray<unknown>, AiError.AiError>;
|
|
110
|
+
export declare const fromChatCompletion: (moduleName: string, body: ChatCompletion, reading?: UsageReading) => Effect.Effect<ReadonlyArray<unknown>, AiError.AiError>;
|
|
80
111
|
export declare const makeCompatLanguageModel: (config: CompatConfig) => Effect.Effect<LanguageModel.LanguageModel>;
|
|
81
112
|
export {};
|
package/dist/llm/compat.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { AiError, LanguageModel, Tool } from "effect/ai";
|
|
2
|
-
import { Effect, Result, Option, Stream } from "effect";
|
|
3
|
-
import { CurrentModelCallPolicy, CurrentPromptCacheKey, describeModel, strictJsonSchema, toolParametersSchema } from "@xandreed/core";
|
|
1
|
+
import { AiError, LanguageModel, Prompt, Tool } from "effect/ai";
|
|
2
|
+
import { Effect, Result, Option, Schema, Stream } from "effect";
|
|
3
|
+
import { CurrentModelCallPolicy, CurrentPromptCacheKey, describeModel, ImageMediaType, mediaLabel, strictJsonSchema, toolParametersSchema } from "@xandreed/core";
|
|
4
4
|
import { finishReasonFromWire, sseStreamParts, usageFromCompletion } from "./sse.js";
|
|
5
|
+
import { redactCredentials } from "../transport/http-proxy.entity.functions.js";
|
|
5
6
|
const record = (value) => typeof value === "object" && value !== null ? value : {}, semanticGatewayError = (moduleName, method, body) => Option.flatMap(Option.fromNullishOr(Result.getOrUndefined(Result.try(() => JSON.parse(body)))), (decoded) => {
|
|
6
7
|
const error = record(record(decoded).error), type = typeof error.type === "string" ? error.type : "", message = typeof error.message === "string" ? error.message : "";
|
|
7
8
|
if (type === "ModelError" || /model .*not supported/i.test(message))
|
|
@@ -19,18 +20,57 @@ const record = (value) => typeof value === "object" && value !== null ? value :
|
|
|
19
20
|
reason: new AiError.UnknownError({ description: `CreditsError: ${message || "provider quota exhausted"}` })
|
|
20
21
|
}));
|
|
21
22
|
return Option.none();
|
|
22
|
-
}), aiUnknown = (moduleName, method, e) => AiError.make({ module: moduleName, method, reason: new AiError.UnknownError({ description: String(e) }) }), invalidOutput = (moduleName, method, description) => AiError.make({ module: moduleName, method, reason: new AiError.InvalidOutputError({ description }) }), requestInfo = (chatUrl) => ({
|
|
23
|
+
}), aiUnknown = (moduleName, method, e) => AiError.make({ module: moduleName, method, reason: new AiError.UnknownError({ description: redactCredentials(String(e)) }) }), invalidOutput = (moduleName, method, description) => AiError.make({ module: moduleName, method, reason: new AiError.InvalidOutputError({ description }) }), requestInfo = (chatUrl) => ({
|
|
23
24
|
method: "POST",
|
|
24
25
|
url: chatUrl,
|
|
25
26
|
urlParams: [],
|
|
26
27
|
headers: { "content-type": "application/json" }
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
}), isImageType = Schema.is(ImageMediaType), decodedBytes = (data) => {
|
|
29
|
+
if (typeof data !== "string")
|
|
30
|
+
return data.length;
|
|
31
|
+
const base64 = data.replace(/^data:[^,]*;base64,/i, "").replace(/\s+/g, ""), padding = base64.endsWith("==") ? 2 : base64.endsWith("=") ? 1 : 0;
|
|
32
|
+
return Math.max(0, Math.floor(base64.length * 3 / 4) - padding);
|
|
33
|
+
}, filePartLabel = (part) => {
|
|
34
|
+
const data = part.data;
|
|
35
|
+
return isImageType(part.mediaType) && !(data instanceof URL) ? mediaLabel({ mediaType: part.mediaType, bytes: decodedBytes(data), ...part.fileName === void 0 ? {} : { name: part.fileName } }) : `[file omitted: ${[part.mediaType, ...part.fileName === void 0 ? [] : [part.fileName]].join(", ")}]`;
|
|
36
|
+
}, encodeBase64 = Schema.encodeSync(Schema.Uint8ArrayFromBase64), isSendableImage = (part) => isImageType(part.mediaType), imageUrlPart = (part) => {
|
|
37
|
+
const data = part.data;
|
|
38
|
+
return { type: "image_url", image_url: { url: data instanceof URL ? data.toString() : data instanceof Uint8Array ? `data:${part.mediaType};base64,${encodeBase64(data)}` : /^(data:|https?:\/\/)/i.test(data) ? data : `data:${part.mediaType};base64,${data}` } };
|
|
39
|
+
}, nonEmpty = (line) => line.length > 0, toolResultText = (part, images) => part.modelOutput === void 0 ? typeof part.result === "string" ? part.result : JSON.stringify(part.result ?? null) : part.modelOutput.flatMap((output) => output.type === "text" ? [output.text] : images && isSendableImage(output) ? [] : [filePartLabel(output)]).filter(nonEmpty).join(`
|
|
40
|
+
`), userText = (message) => {
|
|
41
|
+
const text = message.content.flatMap((p) => p.type === "text" ? [p.text] : []).join(""), labels = message.content.flatMap((p) => p.type === "file" ? [filePartLabel(p)] : []);
|
|
42
|
+
return [text, ...labels].filter(nonEmpty).join(`
|
|
43
|
+
`);
|
|
44
|
+
}, contentParts = (content) => typeof content === "string" ? content.length === 0 ? [] : [{ type: "text", text: content }] : Array.isArray(content) ? content : [], USER_CAPTION = "The user's message:", flush = (fold) => fold.pending.length === 0 ? fold.out : [...fold.out, { role: "user", content: fold.pending }];
|
|
45
|
+
export const toChatMessages = (prompt, options = {}) => {
|
|
46
|
+
const images = options.images ?? !1, userMessage = (message) => {
|
|
47
|
+
if (!(images && message.content.some((p) => p.type === "file" && isSendableImage(p))))
|
|
48
|
+
return { role: "user", content: userText(message) };
|
|
49
|
+
return {
|
|
50
|
+
role: "user",
|
|
51
|
+
content: message.content.flatMap((p) => p.type === "text" ? p.text.length === 0 ? [] : [{ type: "text", text: p.text }] : isSendableImage(p) ? [imageUrlPart(p)] : [{ type: "text", text: filePartLabel(p) }])
|
|
52
|
+
};
|
|
53
|
+
}, folded = prompt.content.reduce((fold, message) => {
|
|
54
|
+
if (message.role === "user") {
|
|
55
|
+
const user = userMessage(message);
|
|
56
|
+
if (fold.pending.length === 0)
|
|
57
|
+
return { out: [...fold.out, user], pending: [] };
|
|
58
|
+
const parts = contentParts(user.content), own = parts.some((part) => typeof part === "object" && part !== null && ("type" in part) && part.type === "image_url");
|
|
59
|
+
return { out: [...fold.out, { role: "user", content: [...fold.pending, ...own ? [{ type: "text", text: USER_CAPTION }] : [], ...parts] }], pending: [] };
|
|
60
|
+
}
|
|
61
|
+
if (message.role === "tool") {
|
|
62
|
+
const results = message.content.flatMap((p) => p.type === "tool-result" ? [p] : []), sent = results.flatMap((p) => {
|
|
63
|
+
const parts = images ? (p.modelOutput ?? []).flatMap((output) => output.type === "file" && isSendableImage(output) ? [output] : []) : [];
|
|
64
|
+
return parts.length === 0 ? [] : [{ type: "text", text: `Images returned by ${p.name} (call ${p.id}):` }, ...parts.map(imageUrlPart)];
|
|
65
|
+
}), toolMessages = results.map((p) => {
|
|
66
|
+
const text = toolResultText(p, images), hasImages = images && (p.modelOutput ?? []).some((output) => output.type === "file" && isSendableImage(output));
|
|
67
|
+
return { role: "tool", tool_call_id: p.id, content: text.length === 0 && hasImages ? "[the images this call returned follow]" : text };
|
|
68
|
+
});
|
|
69
|
+
return { out: [...fold.out, ...toolMessages], pending: [...fold.pending, ...sent] };
|
|
70
|
+
}
|
|
71
|
+
const out = flush(fold);
|
|
72
|
+
if (message.role === "system")
|
|
73
|
+
return { out: [...out, { role: "system", content: message.content }], pending: [] };
|
|
34
74
|
const text = message.content.flatMap((p) => p.type === "text" ? [p.text] : []).join(""), toolCalls = message.content.flatMap((p) => p.type === "tool-call" ? [
|
|
35
75
|
{
|
|
36
76
|
id: p.id,
|
|
@@ -38,22 +78,33 @@ export const toChatMessages = (prompt) => prompt.content.flatMap((message) => {
|
|
|
38
78
|
function: { name: p.name, arguments: JSON.stringify(p.params ?? {}) }
|
|
39
79
|
}
|
|
40
80
|
] : []);
|
|
41
|
-
return
|
|
42
|
-
{
|
|
81
|
+
return {
|
|
82
|
+
out: [...out, {
|
|
43
83
|
role: "assistant",
|
|
44
84
|
content: text.length > 0 ? text : null,
|
|
45
85
|
...toolCalls.length > 0 ? { tool_calls: toolCalls } : {}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
86
|
+
}],
|
|
87
|
+
pending: []
|
|
88
|
+
};
|
|
89
|
+
}, { out: [], pending: [] });
|
|
90
|
+
return flush(folded);
|
|
91
|
+
}, promptWithImageLabels = (prompt) => Prompt.fromMessages(prompt.content.map((message) => {
|
|
92
|
+
if (message.role === "user")
|
|
93
|
+
return message.content.some((p) => p.type === "file") ? Prompt.makeMessage("user", { content: [Prompt.makePart("text", { text: userText(message) })], options: message.options }) : message;
|
|
94
|
+
if (message.role === "tool")
|
|
95
|
+
return message.content.some((p) => p.type === "tool-result" && p.modelOutput !== void 0) ? Prompt.makeMessage("tool", {
|
|
96
|
+
content: message.content.map((p) => p.type !== "tool-result" || p.modelOutput === void 0 ? p : Prompt.makePart("tool-result", {
|
|
97
|
+
id: p.id,
|
|
98
|
+
name: p.name,
|
|
99
|
+
isFailure: p.isFailure,
|
|
100
|
+
result: toolResultText(p, !1),
|
|
101
|
+
providerExecuted: p.providerExecuted,
|
|
102
|
+
options: p.options
|
|
103
|
+
})),
|
|
104
|
+
options: message.options
|
|
105
|
+
}) : message;
|
|
106
|
+
return message;
|
|
107
|
+
})), toChatTools = (tools) => tools.flatMap((tool) => Tool.isUserDefined(tool) ? [
|
|
57
108
|
{
|
|
58
109
|
type: "function",
|
|
59
110
|
function: {
|
|
@@ -81,7 +132,7 @@ export const thinkingParams = (model) => {
|
|
|
81
132
|
if (/qwen/i.test(model))
|
|
82
133
|
return { enable_thinking: !0 };
|
|
83
134
|
return {};
|
|
84
|
-
}, fromChatCompletion = (moduleName, body) => Effect.gen(function* () {
|
|
135
|
+
}, fromChatCompletion = (moduleName, body, reading = {}) => Effect.gen(function* () {
|
|
85
136
|
const choice = body.choices?.[0];
|
|
86
137
|
if (choice === void 0)
|
|
87
138
|
return yield* Effect.fail(invalidOutput(moduleName, "generateText", "the completion carried no choices"));
|
|
@@ -101,7 +152,7 @@ export const thinkingParams = (model) => {
|
|
|
101
152
|
{
|
|
102
153
|
type: "finish",
|
|
103
154
|
reason: toolCalls.length > 0 ? "tool-calls" : finishReasonFromWire(choice.finish_reason),
|
|
104
|
-
usage: usageFromCompletion(body.usage)
|
|
155
|
+
usage: usageFromCompletion(body.usage, reading)
|
|
105
156
|
}
|
|
106
157
|
];
|
|
107
158
|
});
|
|
@@ -124,7 +175,7 @@ const requestSettings = (config, cacheKey, policy) => ({
|
|
|
124
175
|
return {
|
|
125
176
|
model: config.model,
|
|
126
177
|
...settings.sampling,
|
|
127
|
-
messages: toChatMessages(options.prompt),
|
|
178
|
+
messages: toChatMessages(options.prompt, { images: config.images ?? !1 }),
|
|
128
179
|
stream: streaming,
|
|
129
180
|
...options.responseFormat.type === "json" ? { response_format: {
|
|
130
181
|
type: "json_schema",
|
|
@@ -184,7 +235,7 @@ export const makeCompatLanguageModel = (config) => LanguageModel.make({
|
|
|
184
235
|
try: () => JSON.parse(text),
|
|
185
236
|
catch: () => invalidOutput(config.moduleName, "generateText", `the completion body was not JSON: ${text.slice(0, 200)}`)
|
|
186
237
|
});
|
|
187
|
-
return yield* fromChatCompletion(config.moduleName, parsed);
|
|
238
|
+
return yield* fromChatCompletion(config.moduleName, parsed, config.usage);
|
|
188
239
|
}),
|
|
189
240
|
streamText: (options) => Stream.unwrap(Effect.gen(function* () {
|
|
190
241
|
const res = yield* postChat(config, "streamText", yield* chatRequestBody(config, options, !0));
|
|
@@ -193,7 +244,7 @@ export const makeCompatLanguageModel = (config) => LanguageModel.make({
|
|
|
193
244
|
const body = res.body;
|
|
194
245
|
if (body === null)
|
|
195
246
|
return yield* Effect.fail(invalidOutput(config.moduleName, "streamText", "the streaming response carried no body"));
|
|
196
|
-
return sseStreamParts({ moduleName: config.moduleName, body });
|
|
247
|
+
return sseStreamParts({ moduleName: config.moduleName, body, ...config.usage === void 0 ? {} : { usage: config.usage } });
|
|
197
248
|
}))
|
|
198
249
|
}).pipe(Effect.map((model) => describeModel(model, Effect.all({ cacheKey: Effect.service(CurrentPromptCacheKey), policy: Effect.service(CurrentModelCallPolicy) }).pipe(Effect.map(({ cacheKey, policy }) => {
|
|
199
250
|
const settings = requestSettings(config, cacheKey, policy);
|
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
import { Layer } from "effect";
|
|
2
2
|
import { AuthStore, ModelCatalog } from "@xandreed/core";
|
|
3
3
|
import type { Credential, ModelCatalogEntryType } from "@xandreed/core";
|
|
4
|
-
|
|
4
|
+
import type { ModelEndpoints } from "../ports/model-endpoints.port.js";
|
|
5
|
+
/** OpenCode serves its GPT models through the OpenAI Responses protocol, the others through chat completions. */
|
|
6
|
+
export declare const usesOpenCodeResponses: (modelId: string) => boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Whether a built-in route sends a model images: Anthropic's Messages API,
|
|
9
|
+
* the OpenAI Responses routes (`openai`, `openai-codex` but its text-only
|
|
10
|
+
* Spark model, OpenCode's `gpt-*`) and Gemini's chat completions do;
|
|
11
|
+
* OpenCode's other models are text-only until each is verified. A
|
|
12
|
+
* configured endpoint says so itself (`ModelEndpoint.images`). Where it is
|
|
13
|
+
* false, a request's images are sent as label lines.
|
|
14
|
+
*/
|
|
15
|
+
export declare const imageInputFor: (selection: string) => boolean;
|
|
16
|
+
/**
|
|
17
|
+
* The models of every provider with a credential. Each entry's `input` is
|
|
18
|
+
* what its route reads, as `buildProvider` routes it: `endpoints` (the
|
|
19
|
+
* models plugin's configured ones; none by default) replace a built-in
|
|
20
|
+
* route of their name.
|
|
21
|
+
*/
|
|
22
|
+
export declare const configuredModelCatalog: (credentials: ReadonlyMap<string, Credential>, endpoints?: ModelEndpoints) => ReadonlyArray<ModelCatalogEntryType>;
|
|
5
23
|
export type ReasoningEffort = "none" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
6
24
|
/** The reasoning controls accepted by each routed model. `none` on the
|
|
7
25
|
* gpt-5.6 subscription dialect is live-probed (2026-07-16: accepted,
|
|
8
26
|
* reasoning_tokens 0; `minimal` rejected). */
|
|
9
27
|
export declare const reasoningEffortsFor: (selection: string) => ReadonlyArray<ReasoningEffort>;
|
|
28
|
+
/** The catalog over the AuthStore's credentials and the configured endpoints (`CurrentModelEndpoints`). */
|
|
10
29
|
export declare const ConfiguredModelCatalogLive: Layer.Layer<ModelCatalog, never, AuthStore>;
|
package/dist/llm/modelCatalog.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Effect, Layer } from "effect";
|
|
2
|
-
import { AuthStore, ModelCatalog } from "@xandreed/core";
|
|
1
|
+
import { Effect, Layer, Option } from "effect";
|
|
2
|
+
import { AuthStore, ModelCatalog, parseModelSelection } from "@xandreed/core";
|
|
3
|
+
import { CurrentModelEndpoints } from "../ports/model-endpoints.port.js";
|
|
3
4
|
const OPENCODE_MODELS = [
|
|
4
5
|
"glm-5.2",
|
|
5
6
|
"glm-5.1",
|
|
@@ -40,11 +41,22 @@ const OPENCODE_MODELS = [
|
|
|
40
41
|
anthropic: ["claude-fable-5", "claude-haiku-4-5", "claude-opus-4-8", "claude-sonnet-5"],
|
|
41
42
|
google: ["gemini-3-flash", "gemini-3.1-pro", "gemini-3.5-flash"]
|
|
42
43
|
};
|
|
43
|
-
export const
|
|
44
|
+
export const usesOpenCodeResponses = (modelId) => /^gpt-/i.test(modelId);
|
|
45
|
+
const TEXT_ONLY_CODEX = new Set(["gpt-5.3-codex-spark"]);
|
|
46
|
+
export const imageInputFor = (selection) => Option.match(parseModelSelection(selection), {
|
|
47
|
+
onNone: () => !1,
|
|
48
|
+
onSome: ({ provider, modelId }) => provider === "anthropic" || provider === "openai" || provider === "google" || provider === "openai-codex" && !TEXT_ONLY_CODEX.has(modelId) || provider === "opencode" && usesOpenCodeResponses(modelId)
|
|
49
|
+
});
|
|
50
|
+
const routeReadsImages = (provider, model, credential, endpoints) => Option.match(Option.fromNullishOr(endpoints[provider]), {
|
|
51
|
+
onSome: (endpoint) => endpoint.images ?? !1,
|
|
52
|
+
onNone: () => !(credential.type === "local" && credential.baseUrl !== void 0) && imageInputFor(`${provider}:${model}`)
|
|
53
|
+
});
|
|
54
|
+
export const configuredModelCatalog = (credentials, endpoints = {}) => [...credentials].flatMap(([provider, credential]) => (MODELS[provider] ?? []).map((model) => ({
|
|
44
55
|
selection: `${provider}:${model}`,
|
|
45
56
|
label: provider === "openai-codex" ? `OpenAI subscription \xB7 ${model}` : provider === "openai" ? `OpenAI API key \xB7 ${model}` : void 0,
|
|
46
57
|
provider,
|
|
47
|
-
credential: credential.type
|
|
58
|
+
credential: credential.type,
|
|
59
|
+
input: routeReadsImages(provider, model, credential, endpoints) ? ["text", "image"] : ["text"]
|
|
48
60
|
}))), reasoningEffortsFor = (selection) => {
|
|
49
61
|
if (/^openai-codex:gpt-5\.6-(luna|sol|terra)$/.test(selection))
|
|
50
62
|
return ["none", "low", "medium", "high", "xhigh", "max"];
|
|
@@ -53,6 +65,6 @@ export const configuredModelCatalog = (credentials) => [...credentials].flatMap(
|
|
|
53
65
|
if (/^openai:gpt-5/.test(selection))
|
|
54
66
|
return ["low", "medium", "high"];
|
|
55
67
|
return [];
|
|
56
|
-
}, ConfiguredModelCatalogLive = Layer.effect(ModelCatalog, Effect.
|
|
57
|
-
list: auth.all.pipe(Effect.orElseSucceed(() => new Map), Effect.map(configuredModelCatalog))
|
|
58
|
-
})));
|
|
68
|
+
}, ConfiguredModelCatalogLive = Layer.effect(ModelCatalog, Effect.all({ auth: AuthStore, endpoints: Effect.service(CurrentModelEndpoints) }).pipe(Effect.map(({ auth, endpoints }) => ({
|
|
69
|
+
list: auth.all.pipe(Effect.orElseSucceed(() => new Map), Effect.map((credentials) => configuredModelCatalog(credentials, endpoints)))
|
|
70
|
+
}))));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { LanguageModel } from "effect/ai";
|
|
2
2
|
import { HttpClient } from "effect/http";
|
|
3
|
-
import { Effect, Redacted } from "effect";
|
|
3
|
+
import { Effect, Option, Redacted } from "effect";
|
|
4
|
+
import type { WebSocketProxy } from "../ports/models-transport.port.js";
|
|
4
5
|
export declare const OPENAI_CODEX_API_URL = "https://chatgpt.com/backend-api/codex";
|
|
5
6
|
type Json = Record<string, unknown>;
|
|
6
7
|
/** Translate the standard Responses request into the Codex subscription dialect. */
|
|
@@ -10,5 +11,7 @@ export declare const makeOpenAiCodexLanguageModel: (args: {
|
|
|
10
11
|
readonly model: string;
|
|
11
12
|
readonly accessToken: Redacted.Redacted<string>;
|
|
12
13
|
readonly accountId: string;
|
|
14
|
+
/** How the WebSocket reaches ChatGPT (`ModelsTransport.webSocketAgent`); default: directly. */
|
|
15
|
+
readonly webSocketProxy?: Option.Option<WebSocketProxy>;
|
|
13
16
|
}) => Effect.Effect<LanguageModel.LanguageModel, never, HttpClient.HttpClient>;
|
|
14
17
|
export {};
|
package/dist/llm/openAiCodex.js
CHANGED
|
@@ -3,7 +3,7 @@ import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai";
|
|
|
3
3
|
import { HttpClient, HttpClientRequest } from "effect/http";
|
|
4
4
|
import { Effect, Result, Option, Redacted, Stream } from "effect";
|
|
5
5
|
import { CurrentModelCallPolicy, CurrentPromptCacheKey, describeModel, foldStreamParts } from "@xandreed/core";
|
|
6
|
-
import {
|
|
6
|
+
import { makeOpenAiCodexWebSocketHttpClient } from "./openAiCodexWebSocket.js";
|
|
7
7
|
export const OPENAI_CODEX_API_URL = "https://chatgpt.com/backend-api/codex";
|
|
8
8
|
const record = (value) => typeof value === "object" && value !== null ? value : {}, instructionText = (message) => {
|
|
9
9
|
const value = record(message);
|
|
@@ -69,7 +69,7 @@ export const mapOpenAiCodexError = (model, error) => responseStatus(error) === 4
|
|
|
69
69
|
}).pipe(Effect.flatMap(({ cacheKey, policy }) => OpenAiClient.make({
|
|
70
70
|
apiKey: args.accessToken,
|
|
71
71
|
apiUrl: OPENAI_CODEX_API_URL,
|
|
72
|
-
transformClient: () =>
|
|
72
|
+
transformClient: () => makeOpenAiCodexWebSocketHttpClient(args.webSocketProxy ?? Option.none()).pipe(HttpClient.mapRequest((request) => transformRequest(args.accountId, Option.getOrUndefined(Option.map(policy, (value) => value.effort)))(request.pipe(HttpClientRequest.prependUrl(OPENAI_CODEX_API_URL), HttpClientRequest.bearerToken(args.accessToken)))))
|
|
73
73
|
}).pipe(Effect.flatMap((client) => OpenAiLanguageModel.make({
|
|
74
74
|
model: args.model,
|
|
75
75
|
config: {
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { HttpClient } from "effect/http";
|
|
1
|
+
import { HttpClient, HttpClientError } from "effect/http";
|
|
2
|
+
import { Option } from "effect";
|
|
3
|
+
import type { WebSocketProxy } from "../ports/models-transport.port.js";
|
|
4
|
+
type Json = Record<string, unknown>;
|
|
2
5
|
/** ChatGPT's Luna router currently keys its internal route on Pi's
|
|
3
6
|
* time-ordered UUIDv7 session shape; UUIDv4 reaches a nonexistent rollout
|
|
4
7
|
* alias. Keep one v7 id per Efferent conversation/cache key. */
|
|
@@ -6,9 +9,26 @@ export declare const openAiCodexUuidV7: (now?: number) => string;
|
|
|
6
9
|
/** Codex currently names the WebSocket terminal event `response.done`; the
|
|
7
10
|
* public Responses stream consumed by @effect/ai names it `response.completed`. */
|
|
8
11
|
export declare const normalizeOpenAiCodexWebSocketEvent: (value: unknown) => unknown;
|
|
12
|
+
/**
|
|
13
|
+
* The OpenAI error body of an `error` frame that refuses the request, which
|
|
14
|
+
* the HTTP Responses endpoint would answer with a 400: the frame's status is
|
|
15
|
+
* 400, its error's type or code is `invalid_request_error`, or its message
|
|
16
|
+
* names an image. The error is the frame's `error` object, or the frame
|
|
17
|
+
* itself (the Responses stream's own `error` event). Any other frame: none.
|
|
18
|
+
*/
|
|
19
|
+
export declare const openAiCodexRefusal: (event: unknown) => Option.Option<Json>;
|
|
9
20
|
export declare const isOpenAiCodexControlEvent: (type: unknown) => boolean;
|
|
10
21
|
/** Adapt the ChatGPT Codex WebSocket protocol to an SSE-shaped HttpClient
|
|
11
22
|
* response. OpenAiLanguageModel remains the sole protocol decoder, so text,
|
|
12
23
|
* reasoning, tool calls, tool results, usage, and errors keep their standard
|
|
13
|
-
* Effect representations instead of being reimplemented here.
|
|
14
|
-
|
|
24
|
+
* Effect representations instead of being reimplemented here. `proxy` is how
|
|
25
|
+
* the socket reaches ChatGPT (`ModelsTransport.webSocketAgent`); `None`
|
|
26
|
+
* connects directly. A route this runtime cannot use fails every request.
|
|
27
|
+
* The response is the stream once the first event arrives, unless that
|
|
28
|
+
* event is an `error` refusing the request (`openAiCodexRefusal`): the
|
|
29
|
+
* request then fails with a 400 status and the OpenAI error body, as the
|
|
30
|
+
* HTTP endpoint answers it, so a refused image is an `InvalidRequestError`. */
|
|
31
|
+
export declare const makeOpenAiCodexWebSocketHttpClient: (proxy: Option.Option<WebSocketProxy>) => HttpClient.HttpClient.With<HttpClientError.HttpClientError, never>;
|
|
32
|
+
/** The subscription client connecting directly, with no proxy. */
|
|
33
|
+
export declare const OpenAiCodexWebSocketHttpClient: HttpClient.HttpClient.With<HttpClientError.HttpClientError, never>;
|
|
34
|
+
export {};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/http";
|
|
2
|
-
import { Effect, Result } from "effect";
|
|
2
|
+
import { Effect, Match, Option, Redacted, Result } from "effect";
|
|
3
3
|
import { release } from "node:os";
|
|
4
4
|
import WebSocket from "ws";
|
|
5
|
+
import { redactCredentials } from "../transport/http-proxy.entity.functions.js";
|
|
6
|
+
import { currentRuntime } from "../transport/models-transport.adapter.js";
|
|
5
7
|
const WEBSOCKET_BETA = "responses_websockets=2026-02-06", sessionIds = new Map, decodeBody = (request) => {
|
|
6
8
|
if (request.body._tag !== "Uint8Array")
|
|
7
9
|
return {};
|
|
@@ -46,77 +48,129 @@ export const normalizeOpenAiCodexWebSocketEvent = (value) => {
|
|
|
46
48
|
return value;
|
|
47
49
|
const event = value;
|
|
48
50
|
return event.type === "response.done" ? { ...event, type: "response.completed" } : event;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
socket.addEventListener("
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
51
|
+
};
|
|
52
|
+
const recordOf = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? Option.some(value) : Option.none(), stringOf = (value) => typeof value === "string" ? Option.some(value) : Option.none();
|
|
53
|
+
export const openAiCodexRefusal = (event) => Option.flatMap(Option.filter(recordOf(event), (frame) => frame.type === "error"), (frame) => {
|
|
54
|
+
const error = Option.getOrElse(recordOf(frame.error), () => frame), message = Option.getOrElse(stringOf(error.message), () => ""), type = stringOf(error.type), code = stringOf(error.code);
|
|
55
|
+
return frame.status === 400 || /image/i.test(message) || Option.contains(type, "invalid_request_error") || Option.contains(code, "invalid_request_error") ? Option.some({ error: {
|
|
56
|
+
message: message.length > 0 ? message : "the request was refused",
|
|
57
|
+
type: Option.getOrElse(Option.filter(type, (value) => value !== "error"), () => "invalid_request_error"),
|
|
58
|
+
code: Option.getOrNull(code),
|
|
59
|
+
param: Option.getOrNull(stringOf(error.param))
|
|
60
|
+
} }) : Option.none();
|
|
61
|
+
}), isOpenAiCodexControlEvent = (type) => typeof type === "string" && (type.startsWith("codex.") || type === "responsesapi.websocket_timing");
|
|
62
|
+
const wsSocket = (socket) => ({
|
|
63
|
+
send: (data) => socket.send(data),
|
|
64
|
+
close: (code, reason) => socket.close(code, reason),
|
|
65
|
+
onOpen: (listener) => socket.addEventListener("open", () => listener()),
|
|
66
|
+
onMessage: (listener) => socket.addEventListener("message", (event) => listener(event.data)),
|
|
67
|
+
onError: (listener) => socket.addEventListener("error", (event) => listener(event.error instanceof Error ? event.error : Error(event.message || "WebSocket handshake failed"))),
|
|
68
|
+
onClose: (listener) => socket.addEventListener("close", (event) => listener(event.code, event.reason))
|
|
69
|
+
}), bunSocket = (url, options) => {
|
|
70
|
+
const socket = Reflect.construct(globalThis.WebSocket, [url, options]);
|
|
71
|
+
return {
|
|
72
|
+
send: (data) => socket.send(data),
|
|
73
|
+
close: (code, reason) => socket.close(code, reason),
|
|
74
|
+
onOpen: (listener) => socket.addEventListener("open", () => listener()),
|
|
75
|
+
onMessage: (listener) => socket.addEventListener("message", (event) => listener(event.data)),
|
|
76
|
+
onError: (listener) => socket.addEventListener("error", (event) => listener(Error("message" in event && typeof event.message === "string" && event.message.length > 0 ? event.message : "WebSocket handshake failed"))),
|
|
77
|
+
onClose: (listener) => socket.addEventListener("close", (event) => listener(event.code, event.reason))
|
|
78
|
+
};
|
|
79
|
+
}, openSocket = (url, headers, proxy) => Option.match(proxy, {
|
|
80
|
+
onNone: () => wsSocket(new WebSocket(url, { headers })),
|
|
81
|
+
onSome: (route) => Match.value(route).pipe(Match.tag("Agent", ({ agent }) => wsSocket(new WebSocket(url, { headers, agent }))), Match.tag("Proxy", ({ url: proxyUrl }) => bunSocket(url, { headers, proxy: Redacted.value(proxyUrl) })), Match.exhaustive)
|
|
82
|
+
}), routeMismatch = (route) => {
|
|
83
|
+
const runtime = currentRuntime();
|
|
84
|
+
return Match.value(route).pipe(Match.tag("Agent", () => runtime._tag === "Node" ? Option.none() : Option.some(`the WebSocket route is a Node agent, which ${runtime._tag === "Bun" ? "Bun's WebSocket" : "this runtime's WebSocket"} cannot use`)), Match.tag("Proxy", () => runtime._tag === "Bun" ? Option.none() : Option.some("the WebSocket route is a proxy URL for Bun's WebSocket, which this runtime's WebSocket would ignore")), Match.exhaustive, Option.map((why) => `${why}: refused rather than connecting directly past the proxy`));
|
|
85
|
+
}, transportFailure = (request, cause) => new HttpClientError.HttpClientError({
|
|
86
|
+
reason: new HttpClientError.TransportError({
|
|
87
|
+
request: request.pipe(HttpClientRequest.setHeader("authorization", "[redacted]"), HttpClientRequest.setHeader("chatgpt-account-id", "[redacted]")),
|
|
88
|
+
description: `OpenAI subscription WebSocket failed: ${redactCredentials(cause instanceof Error ? cause.message : String(cause))}`,
|
|
89
|
+
cause
|
|
90
|
+
})
|
|
91
|
+
});
|
|
92
|
+
export const makeOpenAiCodexWebSocketHttpClient = (proxy) => HttpClient.make((request, url, signal) => Option.match(Option.flatMap(proxy, routeMismatch), {
|
|
93
|
+
onSome: (why) => Effect.fail(transportFailure(request, Error(why))),
|
|
94
|
+
onNone: () => Effect.tryPromise({
|
|
95
|
+
try: () => new Promise((resolve, reject) => {
|
|
96
|
+
const body = decodeBody(request), safeRequest = request.pipe(HttpClientRequest.setHeader("authorization", "[redacted]"), HttpClientRequest.setHeader("chatgpt-account-id", "[redacted]")), headers = Object.fromEntries(["authorization", "chatgpt-account-id", "originator"].flatMap((key) => {
|
|
97
|
+
const value = request.headers[key];
|
|
98
|
+
return value === void 0 ? [] : [[key, value]];
|
|
99
|
+
}));
|
|
100
|
+
headers["openai-beta"] = WEBSOCKET_BETA;
|
|
101
|
+
headers["user-agent"] = `pi (${process.platform} ${release()}; ${process.arch})`;
|
|
102
|
+
const requestId = sessionIdFor(body.prompt_cache_key);
|
|
103
|
+
body.prompt_cache_key = requestId;
|
|
104
|
+
headers["session-id"] = requestId;
|
|
105
|
+
headers["x-client-request-id"] = requestId;
|
|
106
|
+
const socket = openSocket(websocketUrl(url), headers, proxy), state = { phase: "connecting" }, stream = new ReadableStream({
|
|
107
|
+
start: (value) => {
|
|
108
|
+
state.controller = value;
|
|
109
|
+
},
|
|
110
|
+
cancel: () => socket.close(1000, "cancelled")
|
|
111
|
+
}), respond = (response) => {
|
|
112
|
+
state.phase = "settled";
|
|
113
|
+
resolve(HttpClientResponse.fromWeb(safeRequest, response));
|
|
114
|
+
}, streaming = () => {
|
|
115
|
+
if (state.phase === "sent")
|
|
116
|
+
respond(new Response(stream, { status: 200, headers: { "content-type": "text/event-stream" } }));
|
|
117
|
+
}, fail = (cause) => {
|
|
118
|
+
if (state.phase === "connecting") {
|
|
119
|
+
state.phase = "settled";
|
|
120
|
+
reject(cause);
|
|
121
|
+
} else {
|
|
122
|
+
streaming();
|
|
123
|
+
state.controller?.error(cause);
|
|
96
124
|
}
|
|
97
|
-
|
|
125
|
+
socket.close(1011, "transport error");
|
|
126
|
+
}, abort = () => fail(Error("OpenAI subscription request aborted"));
|
|
127
|
+
signal.addEventListener("abort", abort, { once: !0 });
|
|
128
|
+
socket.onOpen(() => {
|
|
129
|
+
Result.match(Result.try(() => {
|
|
130
|
+
socket.send(JSON.stringify({ type: "response.create", ...body }));
|
|
131
|
+
state.phase = "sent";
|
|
132
|
+
}), { onFailure: fail, onSuccess: () => {
|
|
133
|
+
return;
|
|
134
|
+
} });
|
|
135
|
+
});
|
|
136
|
+
socket.onMessage((data) => {
|
|
137
|
+
Result.match(Result.try(() => {
|
|
138
|
+
const text = eventText(data);
|
|
139
|
+
if (text === void 0)
|
|
140
|
+
return;
|
|
141
|
+
const parsed = normalizeOpenAiCodexWebSocketEvent(JSON.parse(text)), type = parsed.type;
|
|
142
|
+
if (isOpenAiCodexControlEvent(type))
|
|
143
|
+
return;
|
|
144
|
+
const refused = state.phase === "sent" ? openAiCodexRefusal(parsed) : Option.none();
|
|
145
|
+
if (Option.isSome(refused)) {
|
|
146
|
+
respond(new Response(JSON.stringify(refused.value), { status: 400, headers: { "content-type": "application/json" } }));
|
|
147
|
+
signal.removeEventListener("abort", abort);
|
|
148
|
+
socket.close(1000, "refused");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
streaming();
|
|
152
|
+
if (type === "error") {
|
|
153
|
+
fail(Error(JSON.stringify(parsed)));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
state.controller?.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(parsed)}
|
|
98
157
|
|
|
99
158
|
`));
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
reason: new HttpClientError.TransportError({
|
|
117
|
-
request: request.pipe(HttpClientRequest.setHeader("authorization", "[redacted]"), HttpClientRequest.setHeader("chatgpt-account-id", "[redacted]")),
|
|
118
|
-
description: `OpenAI subscription WebSocket failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
119
|
-
cause
|
|
120
|
-
})
|
|
159
|
+
if (type === "response.completed" || type === "response.incomplete" || type === "response.failed") {
|
|
160
|
+
state.controller?.close();
|
|
161
|
+
signal.removeEventListener("abort", abort);
|
|
162
|
+
socket.close(1000, "done");
|
|
163
|
+
}
|
|
164
|
+
}), { onFailure: fail, onSuccess: () => {
|
|
165
|
+
return;
|
|
166
|
+
} });
|
|
167
|
+
});
|
|
168
|
+
socket.onError(fail);
|
|
169
|
+
socket.onClose((code, reason) => {
|
|
170
|
+
if (code !== 1000)
|
|
171
|
+
fail(Error(`OpenAI subscription WebSocket closed ${code}: ${reason}`));
|
|
172
|
+
});
|
|
173
|
+
}),
|
|
174
|
+
catch: (cause) => transportFailure(request, cause)
|
|
121
175
|
})
|
|
122
|
-
}));
|
|
176
|
+
})).pipe(HttpClient.filterStatusOk), OpenAiCodexWebSocketHttpClient = makeOpenAiCodexWebSocketHttpClient(Option.none());
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|