@bitkyc08/opencodex 2.19.0 → 2.20.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/gui/dist/assets/index-DF_UFrGS.css +1 -0
- package/gui/dist/assets/index-DSK3S5HY.js +76 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat.ts +367 -32
- package/src/adapters/registry.ts +144 -0
- package/src/chat/inbound.ts +13 -7
- package/src/cli/claude.ts +2 -1
- package/src/cli/combo.ts +3 -0
- package/src/cli/dispatch.ts +8 -0
- package/src/cli/export-command.ts +2 -2
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +3 -2
- package/src/cli/lab.ts +135 -1
- package/src/cli/minimax.ts +491 -0
- package/src/cli/models-runtime.ts +22 -1
- package/src/cli/models.ts +67 -2
- package/src/cli/opencode.ts +2 -1
- package/src/cli/registry.ts +22 -2
- package/src/clients/config-export.ts +125 -7
- package/src/codex/app-server-processes.ts +57 -2
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/catalog/aggregation.ts +10 -1
- package/src/codex/catalog/effort.ts +15 -3
- package/src/codex/catalog/parsing.ts +3 -1
- package/src/codex/catalog/provider-fetch.ts +45 -5
- package/src/codex/catalog/sync.ts +74 -4
- package/src/codex/convergence.ts +2 -0
- package/src/combos/index.ts +1 -0
- package/src/combos/request.ts +30 -0
- package/src/combos/types.ts +6 -0
- package/src/config.ts +52 -0
- package/src/generated/compatibility-version.json +224 -76
- package/src/images/loop.ts +11 -1
- package/src/integrations/registry.ts +7 -0
- package/src/lab/conformance/jcs.ts +42 -2
- package/src/lab/conformance/negative-controls.ts +6 -2
- package/src/lab/conformance/runner.ts +16 -5
- package/src/lab/fabric/observe.ts +49 -14
- package/src/lab/index.ts +16 -0
- package/src/lab/ledger/purge.ts +152 -83
- package/src/lab/ledger/store.ts +168 -54
- package/src/lab/observe/from-conformance.ts +8 -6
- package/src/lab/observe/from-live.ts +8 -2
- package/src/lab/paths.ts +23 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +219 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/lab-activation.ts +109 -47
- package/src/lib/lab-live-pinned-sender.ts +16 -5
- package/src/lib/pinned-http.ts +70 -16
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/state-store-registrations.ts +2 -0
- package/src/lib/upstream-reachability.ts +4 -0
- package/src/lib/windows-elevation.ts +10 -1
- package/src/providers/derive.ts +24 -4
- package/src/providers/registry.ts +7 -3
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +143 -0
- package/src/providers/static-model-discovery.ts +86 -0
- package/src/reasoning-effort.ts +27 -1
- package/src/router.ts +23 -6
- package/src/routing/capability.ts +4 -2
- package/src/routing/compatibility/behavior.ts +5 -1
- package/src/server/adapter-resolve.ts +2 -32
- package/src/server/auth-cors.ts +8 -0
- package/src/server/chat-completions.ts +74 -36
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +371 -0
- package/src/server/management/combo-routes.ts +16 -2
- package/src/server/management/config-routes.ts +6 -4
- package/src/server/management/context.ts +17 -0
- package/src/server/management/lab-routes.ts +181 -19
- package/src/server/management/model-routes.ts +76 -2
- package/src/server/management/model-rows.ts +8 -0
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +76 -4
- package/src/server/management/system-restart.ts +4 -2
- package/src/server/management/system-routes.ts +38 -0
- package/src/server/relay.ts +17 -3
- package/src/server/responses/compact.ts +4 -1
- package/src/server/responses/core.ts +257 -46
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/fetch-helpers.ts +35 -4
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/policy-fallback.ts +16 -2
- package/src/server/responses/terminal-guard.ts +1 -1
- package/src/server/responses/upstream-error.ts +5 -0
- package/src/server/responses.ts +17 -2
- package/src/types.ts +66 -4
- package/src/update/index.ts +6 -5
- package/src/update/job.ts +5 -6
- package/src/update/notify.ts +5 -3
- package/src/usage/log.ts +11 -1
- package/gui/dist/assets/index-CQ7bIKee.css +0 -1
- package/gui/dist/assets/index-D_JUZLEC.js +0 -76
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { buildOpenAIChatPassthroughRequest, createOpenAIChatAdapter } from "../adapters/openai-chat";
|
|
2
|
+
import type { AdapterRequest, ProviderAdapter } from "../adapters/base";
|
|
3
|
+
import {
|
|
4
|
+
chatCompletionsErrorBody,
|
|
5
|
+
chatCompletionsErrorResponse,
|
|
6
|
+
collectChatCompletion,
|
|
7
|
+
isChatCompletionsStreamError,
|
|
8
|
+
} from "../chat/outbound";
|
|
9
|
+
import { classifyError, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode } from "../lib/errors";
|
|
10
|
+
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
11
|
+
import { redactSecretString } from "../lib/redact";
|
|
12
|
+
import { resolveClientRetryAfter } from "../lib/retry-after";
|
|
13
|
+
import {
|
|
14
|
+
applyUpstreamRecoveryInit,
|
|
15
|
+
fetchWithResetRetry,
|
|
16
|
+
prepareSameTarget429Wait,
|
|
17
|
+
type UpstreamSendRecovery,
|
|
18
|
+
} from "../lib/upstream-retry";
|
|
19
|
+
import {
|
|
20
|
+
isTranslatorBudgetExceededError,
|
|
21
|
+
type TranslatorBudget,
|
|
22
|
+
} from "../lib/translator-budget";
|
|
23
|
+
import {
|
|
24
|
+
hasKeyPoolFailover,
|
|
25
|
+
rateLimitRetryDelayMs,
|
|
26
|
+
rateLimitRetryPolicyFor,
|
|
27
|
+
rotateProviderTransportOn429,
|
|
28
|
+
} from "../providers/key-failover";
|
|
29
|
+
import type { RouteResult } from "../router";
|
|
30
|
+
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
31
|
+
import { fetchWithHeaderTimeout, providerFetch, safeHostLabel } from "./responses/fetch-helpers";
|
|
32
|
+
import { linkAbortSignal } from "./responses";
|
|
33
|
+
import {
|
|
34
|
+
addFinalRequestLog,
|
|
35
|
+
beginRequestAttempt,
|
|
36
|
+
noteAttemptSend,
|
|
37
|
+
recordFirstOutput,
|
|
38
|
+
sealRequestAttemptIdentity,
|
|
39
|
+
type RequestLogContext,
|
|
40
|
+
} from "./request-log";
|
|
41
|
+
import { jsonCompletionSse, nativeChatSse, structuredError, usageFromChat } from "./chat-native-sse";
|
|
42
|
+
|
|
43
|
+
type Rec = Record<string, unknown>;
|
|
44
|
+
|
|
45
|
+
const MAX_NATIVE_CHAT_JSON_BYTES = 32 * 1024 * 1024;
|
|
46
|
+
const MAX_NATIVE_CHAT_ERROR_BYTES = 64 * 1024;
|
|
47
|
+
|
|
48
|
+
function isRec(value: unknown): value is Rec {
|
|
49
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function isNativeChatRouteEligible(route: RouteResult, rawBody: Rec): boolean {
|
|
53
|
+
const provider = route.provider;
|
|
54
|
+
if (provider.adapter !== "openai-chat") return false;
|
|
55
|
+
if (provider.authMode !== undefined && provider.authMode !== "key" && provider.authMode !== "local") return false;
|
|
56
|
+
// Combo and policy execution own multi-candidate retries in the Responses pipeline.
|
|
57
|
+
if (route.combo || route.routeKind === "combo" || route.routeKind === "policy") return false;
|
|
58
|
+
if (rawBody.store === true || rawBody.background === true) return false;
|
|
59
|
+
if (typeof rawBody.previous_response_id === "string" && rawBody.previous_response_id.length > 0) return false;
|
|
60
|
+
if (rawBody.compaction_trigger !== undefined) return false;
|
|
61
|
+
if (Array.isArray(rawBody.tools)) {
|
|
62
|
+
for (const tool of rawBody.tools) {
|
|
63
|
+
if (!isRec(tool)) continue;
|
|
64
|
+
if (tool.type === "web_search" || tool.type === "web_search_preview" || tool.type === "image_generation") {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function chatCompletionJson(value: unknown): Rec | null {
|
|
73
|
+
if (!isRec(value) || !Array.isArray(value.choices) || value.choices.length === 0) return null;
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface HandleNativeChatOptions {
|
|
78
|
+
req: Request;
|
|
79
|
+
config: OcxConfig;
|
|
80
|
+
logCtx: RequestLogContext;
|
|
81
|
+
logIds?: { requestId: string; start: number };
|
|
82
|
+
route: RouteResult;
|
|
83
|
+
chatBody: Rec;
|
|
84
|
+
requestedModel: string;
|
|
85
|
+
requestedStream: boolean;
|
|
86
|
+
translatorBudget: TranslatorBudget;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function handleNativeChatCompletions(options: HandleNativeChatOptions): Promise<Response> {
|
|
90
|
+
const { req, config, logCtx, logIds, route, requestedModel, requestedStream, translatorBudget } = options;
|
|
91
|
+
logCtx.inboundProtocol = "chat";
|
|
92
|
+
const attempt = beginRequestAttempt(
|
|
93
|
+
(logCtx.attempts?.length ?? 0) + 1,
|
|
94
|
+
route.providerName,
|
|
95
|
+
route.modelId,
|
|
96
|
+
"openai-chat",
|
|
97
|
+
);
|
|
98
|
+
logCtx.activeAttempt = attempt;
|
|
99
|
+
logCtx.activeAttemptStartedAt = Date.now();
|
|
100
|
+
(logCtx.attempts ??= []).push(attempt);
|
|
101
|
+
sealRequestAttemptIdentity(attempt, route.providerName, "openai-chat", logCtx.accountLogLabel);
|
|
102
|
+
|
|
103
|
+
let logged = false;
|
|
104
|
+
const finishLog = (status: number, message?: string, closeReason: "non_stream" | "terminal" | "client_cancel" = "non_stream") => {
|
|
105
|
+
if (logged) return;
|
|
106
|
+
logged = true;
|
|
107
|
+
if (message) logCtx.upstreamError = redactSecretString(message).slice(0, 500);
|
|
108
|
+
if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, { closeReason });
|
|
109
|
+
};
|
|
110
|
+
const fail = (status: number, message: string, type?: string, code?: string | null): Response => {
|
|
111
|
+
const safeMessage = redactSecretString(message);
|
|
112
|
+
finishLog(status, safeMessage);
|
|
113
|
+
return chatCompletionsErrorResponse(status, safeMessage, type, code);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
logCtx.requestedEffort = typeof options.chatBody.reasoning_effort === "string"
|
|
117
|
+
? options.chatBody.reasoning_effort
|
|
118
|
+
: undefined;
|
|
119
|
+
logCtx.requestedServiceTier = typeof options.chatBody.service_tier === "string"
|
|
120
|
+
? options.chatBody.service_tier
|
|
121
|
+
: undefined;
|
|
122
|
+
|
|
123
|
+
const upstream = new AbortController();
|
|
124
|
+
const cleanupAbort = linkAbortSignal(upstream, req.signal);
|
|
125
|
+
const connectMs = config.connectTimeoutMs ?? 200_000;
|
|
126
|
+
let activeProvider: OcxProviderConfig = route.provider;
|
|
127
|
+
let activeAdapter: ProviderAdapter = createOpenAIChatAdapter(activeProvider);
|
|
128
|
+
let activeRequest: AdapterRequest;
|
|
129
|
+
let retainedRequestBytes = 0;
|
|
130
|
+
const releaseRetainedRequest = () => {
|
|
131
|
+
if (retainedRequestBytes === 0) return;
|
|
132
|
+
translatorBudget.releaseRetained(retainedRequestBytes, { kind: "request_copies" });
|
|
133
|
+
retainedRequestBytes = 0;
|
|
134
|
+
};
|
|
135
|
+
const retainRequest = (request: AdapterRequest) => {
|
|
136
|
+
const bytes = new TextEncoder().encode(request.body).byteLength;
|
|
137
|
+
translatorBudget.chargeRetained(bytes, { kind: "request_copies" });
|
|
138
|
+
retainedRequestBytes = bytes;
|
|
139
|
+
};
|
|
140
|
+
try {
|
|
141
|
+
activeRequest = buildOpenAIChatPassthroughRequest(activeProvider, options.chatBody, route.modelId, requestedStream);
|
|
142
|
+
retainRequest(activeRequest);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
releaseRetainedRequest();
|
|
145
|
+
cleanupAbort();
|
|
146
|
+
upstream.abort();
|
|
147
|
+
if (isTranslatorBudgetExceededError(error)) {
|
|
148
|
+
return fail(413, "request translation buffer exceeded the safe limit", "request_too_large", "translation_buffer_limit");
|
|
149
|
+
}
|
|
150
|
+
return fail(400, error instanceof Error ? error.message : String(error), "invalid_request_error");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const send = async (request: AdapterRequest, recovery?: "rate-limit-429" | "key-429"): Promise<Response> => {
|
|
154
|
+
try {
|
|
155
|
+
return await fetchWithResetRetry(
|
|
156
|
+
(transportRecovery?: UpstreamSendRecovery) => {
|
|
157
|
+
noteAttemptSend(attempt, logCtx.usageLogInputTokens, transportRecovery ?? recovery);
|
|
158
|
+
return fetchWithHeaderTimeout(
|
|
159
|
+
request.url,
|
|
160
|
+
applyUpstreamRecoveryInit({
|
|
161
|
+
method: request.method,
|
|
162
|
+
headers: request.headers,
|
|
163
|
+
body: request.body,
|
|
164
|
+
}, transportRecovery),
|
|
165
|
+
upstream.signal,
|
|
166
|
+
connectMs,
|
|
167
|
+
requestedStream,
|
|
168
|
+
providerFetch(activeProvider, undefined, {
|
|
169
|
+
providerName: route.providerName,
|
|
170
|
+
modelId: route.modelId,
|
|
171
|
+
}),
|
|
172
|
+
);
|
|
173
|
+
},
|
|
174
|
+
{ abortSignal: upstream.signal, label: safeHostLabel(request.url) },
|
|
175
|
+
);
|
|
176
|
+
} finally {
|
|
177
|
+
request.releaseBodyObservation?.();
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
let response: Response;
|
|
182
|
+
try {
|
|
183
|
+
response = await send(activeRequest);
|
|
184
|
+
const retryPolicy = rateLimitRetryPolicyFor(activeProvider);
|
|
185
|
+
let retries = 0;
|
|
186
|
+
while (response.status === 429 && retryPolicy && retries < retryPolicy.attempts) {
|
|
187
|
+
retries += 1;
|
|
188
|
+
for await (const _ of prepareSameTarget429Wait({
|
|
189
|
+
body: response.body,
|
|
190
|
+
signal: upstream.signal,
|
|
191
|
+
delayMs: rateLimitRetryDelayMs(retryPolicy, response.headers.get("retry-after"), Date.now()),
|
|
192
|
+
})) { /* pre-stream wait */ }
|
|
193
|
+
if (upstream.signal.aborted) throw upstream.signal.reason;
|
|
194
|
+
response = await send(activeRequest, "rate-limit-429");
|
|
195
|
+
}
|
|
196
|
+
while (response.status === 429 && hasKeyPoolFailover(activeProvider)) {
|
|
197
|
+
const rotated = rotateProviderTransportOn429(config, route.providerName, activeProvider, {
|
|
198
|
+
retryAfter: response.headers.get("retry-after"),
|
|
199
|
+
now: Date.now(),
|
|
200
|
+
attemptedKey: activeProvider.apiKey,
|
|
201
|
+
promptCacheKey: typeof options.chatBody.prompt_cache_key === "string" ? options.chatBody.prompt_cache_key : undefined,
|
|
202
|
+
});
|
|
203
|
+
if (!rotated) break;
|
|
204
|
+
try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
|
|
205
|
+
activeProvider = rotated;
|
|
206
|
+
activeAdapter = createOpenAIChatAdapter(activeProvider);
|
|
207
|
+
releaseRetainedRequest();
|
|
208
|
+
activeRequest = buildOpenAIChatPassthroughRequest(activeProvider, options.chatBody, route.modelId, requestedStream);
|
|
209
|
+
retainRequest(activeRequest);
|
|
210
|
+
response = await send(activeRequest, "key-429");
|
|
211
|
+
}
|
|
212
|
+
} catch (error) {
|
|
213
|
+
releaseRetainedRequest();
|
|
214
|
+
cleanupAbort();
|
|
215
|
+
upstream.abort();
|
|
216
|
+
if (req.signal.aborted) return fail(499, "Client cancelled request", "client_cancelled");
|
|
217
|
+
return fail(502, error instanceof Error ? error.message : String(error), "server_error");
|
|
218
|
+
}
|
|
219
|
+
releaseRetainedRequest();
|
|
220
|
+
|
|
221
|
+
if (!response.ok) {
|
|
222
|
+
let bodyText = "";
|
|
223
|
+
try {
|
|
224
|
+
const body = await readBoundedResponseBody(response, {
|
|
225
|
+
signal: upstream.signal,
|
|
226
|
+
maxBytes: MAX_NATIVE_CHAT_ERROR_BYTES,
|
|
227
|
+
});
|
|
228
|
+
if (body.displaySafe) bodyText = body.text;
|
|
229
|
+
} catch { /* status-only fallback */ }
|
|
230
|
+
cleanupAbort();
|
|
231
|
+
if (req.signal.aborted) {
|
|
232
|
+
upstream.abort();
|
|
233
|
+
return fail(499, "Client cancelled request", "client_cancelled");
|
|
234
|
+
}
|
|
235
|
+
const detail = activeAdapter.formatErrorBody?.(response.status, response.headers, bodyText) ?? "";
|
|
236
|
+
let upstreamType: string | undefined;
|
|
237
|
+
let upstreamCode: string | null | undefined;
|
|
238
|
+
try {
|
|
239
|
+
const parsedError = JSON.parse(bodyText) as Rec;
|
|
240
|
+
const nested = isRec(parsedError.error) ? parsedError.error : undefined;
|
|
241
|
+
if (typeof nested?.type === "string") upstreamType = nested.type;
|
|
242
|
+
if (nested?.code === null || typeof nested?.code === "string") upstreamCode = nested.code;
|
|
243
|
+
} catch { /* keep generic classification */ }
|
|
244
|
+
const message = detail ? `Provider error ${response.status}: ${detail}` : `Provider error ${response.status}`;
|
|
245
|
+
const classified = classifyError(
|
|
246
|
+
response.status,
|
|
247
|
+
upstreamType ?? (response.status === 401 ? "authentication_error"
|
|
248
|
+
: response.status === 429 ? "rate_limit_error"
|
|
249
|
+
: response.status >= 500 ? "server_error" : "invalid_request_error"),
|
|
250
|
+
message,
|
|
251
|
+
);
|
|
252
|
+
if (isCyberPolicyCode(upstreamCode)) {
|
|
253
|
+
classified.code = CYBER_POLICY_ERROR_CODE;
|
|
254
|
+
classified.type = "invalid_request_error";
|
|
255
|
+
} else if (upstreamCode === "model_not_found") {
|
|
256
|
+
classified.code = "model_not_found";
|
|
257
|
+
classified.type = "invalid_request_error";
|
|
258
|
+
} else if (upstreamCode !== undefined && upstreamCode !== null && classified.code == null) {
|
|
259
|
+
classified.code = upstreamCode;
|
|
260
|
+
}
|
|
261
|
+
const status = isCyberPolicyCode(classified.code) ? 400 : response.status;
|
|
262
|
+
const retryAfter = resolveClientRetryAfter({
|
|
263
|
+
status: response.status,
|
|
264
|
+
message: classified.message,
|
|
265
|
+
upstreamRetryAfter: response.headers.get("retry-after"),
|
|
266
|
+
});
|
|
267
|
+
finishLog(status, classified.message);
|
|
268
|
+
return new Response(JSON.stringify(chatCompletionsErrorBody(status, classified.message, classified.type, classified.code)), {
|
|
269
|
+
status,
|
|
270
|
+
headers: {
|
|
271
|
+
"Content-Type": "application/json",
|
|
272
|
+
...(retryAfter ? { "Retry-After": retryAfter } : {}),
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
278
|
+
if (contentType.includes("text/event-stream") && response.body) {
|
|
279
|
+
const stream = nativeChatSse(response.body, {
|
|
280
|
+
requestedModel,
|
|
281
|
+
translatorBudget,
|
|
282
|
+
signal: upstream.signal,
|
|
283
|
+
onFirstOutput: logIds ? () => recordFirstOutput(logCtx, logIds.start) : undefined,
|
|
284
|
+
onUsage: usage => {
|
|
285
|
+
logCtx.usage = usage;
|
|
286
|
+
attempt.usage = usage;
|
|
287
|
+
},
|
|
288
|
+
...(requestedStream ? {
|
|
289
|
+
onTerminal: (status: number, message?: string) => {
|
|
290
|
+
cleanupAbort();
|
|
291
|
+
finishLog(status, message, "terminal");
|
|
292
|
+
},
|
|
293
|
+
onCancel: () => {
|
|
294
|
+
cleanupAbort();
|
|
295
|
+
upstream.abort();
|
|
296
|
+
finishLog(499, undefined, "client_cancel");
|
|
297
|
+
},
|
|
298
|
+
} : {}),
|
|
299
|
+
});
|
|
300
|
+
if (requestedStream) {
|
|
301
|
+
return new Response(stream, {
|
|
302
|
+
status: 200,
|
|
303
|
+
headers: {
|
|
304
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
305
|
+
"Cache-Control": "no-cache",
|
|
306
|
+
Connection: "keep-alive",
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
const completion = await collectChatCompletion(stream, requestedModel, translatorBudget);
|
|
312
|
+
cleanupAbort();
|
|
313
|
+
finishLog(200);
|
|
314
|
+
return Response.json(completion);
|
|
315
|
+
} catch (error) {
|
|
316
|
+
cleanupAbort();
|
|
317
|
+
upstream.abort();
|
|
318
|
+
if (isChatCompletionsStreamError(error)) {
|
|
319
|
+
return fail(error.status, error.message, error.type, error.code);
|
|
320
|
+
}
|
|
321
|
+
return fail(502, error instanceof Error ? error.message : String(error), "upstream_error");
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
let body;
|
|
326
|
+
try {
|
|
327
|
+
body = await readBoundedResponseBody(response, {
|
|
328
|
+
signal: upstream.signal,
|
|
329
|
+
maxBytes: MAX_NATIVE_CHAT_JSON_BYTES,
|
|
330
|
+
totalTimeoutMs: Math.max(connectMs, 5_000),
|
|
331
|
+
inactivityTimeoutMs: Math.max(connectMs, 5_000),
|
|
332
|
+
});
|
|
333
|
+
} catch (error) {
|
|
334
|
+
cleanupAbort();
|
|
335
|
+
upstream.abort();
|
|
336
|
+
if (req.signal.aborted) return fail(499, "Client cancelled request", "client_cancelled");
|
|
337
|
+
return fail(502, error instanceof Error ? error.message : String(error), "upstream_error");
|
|
338
|
+
}
|
|
339
|
+
cleanupAbort();
|
|
340
|
+
if (body.oversized) {
|
|
341
|
+
upstream.abort();
|
|
342
|
+
return fail(502, "upstream response exceeded the safe limit", "upstream_error", "translation_buffer_limit");
|
|
343
|
+
}
|
|
344
|
+
let parsedJson: unknown;
|
|
345
|
+
try {
|
|
346
|
+
parsedJson = JSON.parse(body.text);
|
|
347
|
+
} catch {
|
|
348
|
+
return fail(502, "upstream returned malformed Chat Completions JSON", "upstream_error");
|
|
349
|
+
}
|
|
350
|
+
const error = structuredError(parsedJson);
|
|
351
|
+
if (error) return fail(error.status ?? 502, error.message, error.type, error.code);
|
|
352
|
+
const completion = chatCompletionJson(parsedJson);
|
|
353
|
+
if (!completion) return fail(502, "upstream response contained no choices", "upstream_error");
|
|
354
|
+
const usage = usageFromChat(completion.usage);
|
|
355
|
+
if (usage) {
|
|
356
|
+
logCtx.usage = usage;
|
|
357
|
+
attempt.usage = usage;
|
|
358
|
+
}
|
|
359
|
+
if (logIds) recordFirstOutput(logCtx, logIds.start);
|
|
360
|
+
finishLog(200);
|
|
361
|
+
if (requestedStream) {
|
|
362
|
+
return new Response(jsonCompletionSse(completion, requestedModel), {
|
|
363
|
+
status: 200,
|
|
364
|
+
headers: { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache" },
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
return new Response(JSON.stringify(completion), {
|
|
368
|
+
status: 200,
|
|
369
|
+
headers: { "Content-Type": "application/json" },
|
|
370
|
+
});
|
|
371
|
+
}
|
|
@@ -65,6 +65,16 @@ import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, C
|
|
|
65
65
|
import type { ManagementContext } from "./context";
|
|
66
66
|
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
67
67
|
|
|
68
|
+
|
|
69
|
+
/** Management wire shape: omit default imageInput "auto" (persist/response sparse). */
|
|
70
|
+
function sparseComboConfig<T extends { imageInput?: "auto" | "disabled" }>(combo: T): Omit<T, "imageInput"> & { imageInput?: "disabled" } {
|
|
71
|
+
const { imageInput, ...rest } = combo;
|
|
72
|
+
return {
|
|
73
|
+
...rest,
|
|
74
|
+
...(imageInput === "disabled" ? { imageInput: "disabled" as const } : {}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
68
78
|
export async function handleComboRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
69
79
|
const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
|
|
70
80
|
|
|
@@ -75,7 +85,7 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
75
85
|
return {
|
|
76
86
|
id,
|
|
77
87
|
model: comboPublicModelId(id, combo),
|
|
78
|
-
...combo,
|
|
88
|
+
...sparseComboConfig(combo),
|
|
79
89
|
};
|
|
80
90
|
}) });
|
|
81
91
|
}
|
|
@@ -124,10 +134,12 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
124
134
|
});
|
|
125
135
|
if (error) return jsonResponse({ error }, 400);
|
|
126
136
|
const normalized = normalizeComboConfig(body.combo as import("../../types").OcxComboConfig);
|
|
137
|
+
// Persist only non-default identity/capability fields so config stays sparse.
|
|
127
138
|
const {
|
|
128
139
|
alias: normalizedAlias,
|
|
129
140
|
nativeAlias: normalizedNativeAlias,
|
|
130
141
|
displayName: normalizedDisplayName,
|
|
142
|
+
imageInput: normalizedImageInput,
|
|
131
143
|
...normalizedBase
|
|
132
144
|
} = normalized;
|
|
133
145
|
const stored: import("../../types").OcxComboConfig = {
|
|
@@ -135,6 +147,7 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
135
147
|
...(normalizedAlias ? { alias: normalizedAlias } : {}),
|
|
136
148
|
...(normalizedNativeAlias ? { nativeAlias: true } : {}),
|
|
137
149
|
...(normalizedDisplayName ? { displayName: normalizedDisplayName } : {}),
|
|
150
|
+
...(normalizedImageInput === "disabled" ? { imageInput: "disabled" as const } : {}),
|
|
138
151
|
};
|
|
139
152
|
const sourceId = renameFrom ?? id;
|
|
140
153
|
const previous = config.combos?.[sourceId];
|
|
@@ -221,7 +234,8 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
221
234
|
}
|
|
222
235
|
const catalogRefresh = await convergeCodexCatalog();
|
|
223
236
|
if (shouldSyncClaudeAgentDefs) await syncClaudeAgentDefsBestEffort();
|
|
224
|
-
|
|
237
|
+
// Wire shape matches persistence: omit default imageInput "auto".
|
|
238
|
+
return jsonResponse({ success: true, id, model: newPublicModel, combo: sparseComboConfig(stored), catalogRefresh });
|
|
225
239
|
}
|
|
226
240
|
|
|
227
241
|
if (url.pathname === "/api/combos" && req.method === "DELETE") {
|
|
@@ -85,6 +85,7 @@ import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from ".
|
|
|
85
85
|
import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
|
|
86
86
|
import type { PersistedUsageAttempt } from "../../usage/log";
|
|
87
87
|
import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
|
|
88
|
+
import { withProviderServiceTierDTO } from "./provider-capability-config";
|
|
88
89
|
import { applySystemEnvToggle } from "../system-env";
|
|
89
90
|
import { getCachedStartupHealth, invalidateStartupHealthCache } from "../startup-health-cache";
|
|
90
91
|
import { runWindowsTrayAction } from "../windows-tray-control";
|
|
@@ -134,8 +135,9 @@ function publicVisionSidecarSettings(
|
|
|
134
135
|
|
|
135
136
|
export async function handleConfigRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
136
137
|
const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
|
|
138
|
+
const readStartupHealth = deps.getCachedStartupHealth ?? getCachedStartupHealth;
|
|
137
139
|
if (url.pathname === "/api/config" && req.method === "GET") {
|
|
138
|
-
return jsonResponse(safeConfigDTO(config));
|
|
140
|
+
return jsonResponse(withProviderServiceTierDTO(safeConfigDTO(config), config));
|
|
139
141
|
}
|
|
140
142
|
|
|
141
143
|
if (url.pathname === "/api/config" && req.method === "PUT") {
|
|
@@ -189,7 +191,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
189
191
|
streamMode: config.streamMode ?? "auto",
|
|
190
192
|
appOwnedMemoryBudgetMb: config.appOwnedMemoryBudgetMb ?? 256,
|
|
191
193
|
codexAccountPickerEnabled: codexAccountPickerEnabled(config),
|
|
192
|
-
startupHealth: await
|
|
194
|
+
startupHealth: await readStartupHealth(config),
|
|
193
195
|
codexRuntime: {
|
|
194
196
|
path: displayCodexRuntimePath(resolved.runtime.command),
|
|
195
197
|
version: resolved.runtime.version,
|
|
@@ -211,7 +213,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
211
213
|
}
|
|
212
214
|
|
|
213
215
|
if (url.pathname === "/api/startup-health" && req.method === "GET") {
|
|
214
|
-
return jsonResponse(await
|
|
216
|
+
return jsonResponse(await readStartupHealth(config));
|
|
215
217
|
}
|
|
216
218
|
|
|
217
219
|
if (url.pathname === "/api/startup-action" && req.method === "POST") {
|
|
@@ -368,7 +370,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
368
370
|
appOwnedMemoryBudgetMb: config.appOwnedMemoryBudgetMb ?? 256,
|
|
369
371
|
codexAccountPickerEnabled: pickerIsEnabled,
|
|
370
372
|
catalogRefreshPending,
|
|
371
|
-
startupHealth: await
|
|
373
|
+
startupHealth: await readStartupHealth(config),
|
|
372
374
|
});
|
|
373
375
|
}
|
|
374
376
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { OcxConfig } from "../../types";
|
|
2
2
|
import type { NativeProfileApiDeps } from "../../codex/native-profile-api";
|
|
3
|
+
import type { StartupHealth } from "../../codex/autostart-health";
|
|
3
4
|
import type { StartupInstallAction } from "../startup-action-control";
|
|
4
5
|
import type { ManagementPrincipal } from "../management-auth";
|
|
5
6
|
import type { CatalogModel } from "../../codex/catalog";
|
|
@@ -7,11 +8,17 @@ import type { injectGrokConfig } from "../../grok/inject";
|
|
|
7
8
|
import type { removeDesktop3pStandardPivot, writeDesktop3pConfig } from "../../claude/desktop-3p";
|
|
8
9
|
import type { RuntimePortState } from "../../config";
|
|
9
10
|
import type { CatalogDisposition, ConvergeCodex } from "../../codex/convergence-types";
|
|
11
|
+
import type {
|
|
12
|
+
performCodexRestart,
|
|
13
|
+
readCodexAppServerState,
|
|
14
|
+
} from "../../codex/app-server-restart-service";
|
|
10
15
|
|
|
11
16
|
export interface ManagementApiDeps {
|
|
12
17
|
toggleCodexMultiAgentV2?: (enabled: boolean) => void;
|
|
13
18
|
toggleDefaultModeRequestUserInput?: (enabled: boolean) => void;
|
|
14
19
|
createManagementConvergeCodex?: (config: Readonly<OcxConfig>) => ConvergeCodex;
|
|
20
|
+
/** Startup-health seam keeps route tests from launching platform probes. */
|
|
21
|
+
getCachedStartupHealth?: (config: Pick<OcxConfig, "codexAutoStart">) => Promise<StartupHealth>;
|
|
15
22
|
/**
|
|
16
23
|
* Persistence seam for route-level tests. Production leaves this unset and uses
|
|
17
24
|
* `saveConfigPreservingClaudeCode`; tests that pass an in-memory fixture config
|
|
@@ -52,6 +59,16 @@ export interface ManagementApiDeps {
|
|
|
52
59
|
* Native-main profile persistence seam for server-boundary tests. Production
|
|
53
60
|
* leaves this unset, so the route creates its normal NativeProfileManager.
|
|
54
61
|
*/
|
|
62
|
+
/**
|
|
63
|
+
* Codex app-server restart seam (devlog/_plan/260815_gui_codex_restart).
|
|
64
|
+
* Grouped rather than three separate fields: the route is an adapter over one
|
|
65
|
+
* service, and a route test that could not stub it would really terminate the
|
|
66
|
+
* developer's own Codex app-servers.
|
|
67
|
+
*/
|
|
68
|
+
codexRestartService?: {
|
|
69
|
+
readState: typeof readCodexAppServerState;
|
|
70
|
+
performRestart: typeof performCodexRestart;
|
|
71
|
+
};
|
|
55
72
|
nativeProfileApi?: NativeProfileApiDeps;
|
|
56
73
|
}
|
|
57
74
|
|