@bitkyc08/opencodex 2.10.0 → 2.10.1
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/AGENTS_INSTALL.md +77 -0
- package/README.md +4 -10
- package/bin/ocx.mjs +71 -18
- package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
- package/gui/dist/assets/index-ChZQsmBY.js +70 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +8 -4
- package/src/adapters/anthropic.ts +208 -14
- package/src/adapters/base.ts +16 -5
- package/src/adapters/cursor/effort-map.ts +3 -2
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/live-transport.ts +105 -95
- package/src/adapters/cursor/native-exec.ts +32 -6
- package/src/adapters/cursor/protobuf-request.ts +20 -15
- package/src/adapters/cursor/request-builder.ts +21 -7
- package/src/adapters/cursor/types.ts +7 -0
- package/src/adapters/google-antigravity-replay.ts +237 -21
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +50 -9
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +10 -2
- package/src/adapters/kiro-tools.ts +10 -1
- package/src/adapters/kiro.ts +37 -11
- package/src/adapters/openai-chat.ts +284 -83
- package/src/adapters/openai-responses.ts +182 -24
- package/src/bridge.ts +177 -7
- package/src/chat/outbound.ts +78 -23
- package/src/claude/agents-inject.ts +27 -5
- package/src/claude/inbound.ts +11 -1
- package/src/claude/model-info.ts +13 -10
- package/src/claude/outbound.ts +17 -0
- package/src/cli/account-api.ts +24 -0
- package/src/cli/account-auth.ts +31 -6
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +5 -0
- package/src/cli/claude.ts +2 -1
- package/src/cli/doctor.ts +93 -22
- package/src/cli/export-command.ts +26 -12
- package/src/cli/help.ts +8 -6
- package/src/cli/index.ts +56 -22
- package/src/cli/integrations.ts +84 -1
- package/src/cli/observe.ts +54 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +6 -3
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +1 -1
- package/src/cli/v2.ts +36 -9
- package/src/clients/config-export.ts +687 -10
- package/src/codex/account-lifecycle.ts +30 -5
- package/src/codex/account-usability.ts +22 -2
- package/src/codex/admission.ts +255 -0
- package/src/codex/auth-api.ts +427 -140
- package/src/codex/auth-context.ts +155 -30
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +62 -0
- package/src/codex/catalog/aggregation.ts +14 -1
- package/src/codex/catalog/bundled.ts +282 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/metadata.ts +51 -6
- package/src/codex/catalog/parsing.ts +6 -3
- package/src/codex/catalog/provider-fetch.ts +576 -41
- package/src/codex/catalog/sync.ts +505 -66
- package/src/codex/catalog-admission.ts +197 -0
- package/src/codex/catalog-write-serialization.ts +241 -0
- package/src/codex/catalog.ts +2 -1
- package/src/codex/codex-write-lock.ts +372 -0
- package/src/codex/convergence-types.ts +593 -0
- package/src/codex/convergence.ts +441 -0
- package/src/codex/desired-state.ts +177 -0
- package/src/codex/features.ts +52 -8
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +257 -0
- package/src/codex/history-lock.ts +241 -0
- package/src/codex/history-migration-guardian.ts +18 -5
- package/src/codex/history-provider.ts +9 -2
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +176 -0
- package/src/codex/inject-coordination.ts +245 -0
- package/src/codex/inject.ts +605 -124
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +10 -1
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/management-convergence.ts +114 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +167 -0
- package/src/codex/native-main-lock-file.ts +158 -0
- package/src/codex/native-main-owner.ts +315 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1512 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +340 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +557 -0
- package/src/codex/project-config-warnings.ts +18 -4
- package/src/codex/prompt-journal.ts +311 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +224 -0
- package/src/codex/quota.ts +86 -3
- package/src/codex/routing.ts +299 -62
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +39 -13
- package/src/codex/subagent-model-fallback.ts +73 -12
- package/src/codex/transition-state.ts +604 -0
- package/src/codex/upstream-host-health.ts +70 -0
- package/src/codex/user-identity.ts +266 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/config.ts +562 -26
- package/src/generated/jawcode-model-metadata.ts +2 -2
- package/src/grok/inject.ts +15 -4
- package/src/grok/inspect.ts +45 -0
- package/src/images/loop.ts +113 -20
- package/src/integrations/config-io.ts +151 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/native/ownership-preflight.ts +165 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +101 -0
- package/src/integrations/serialize.ts +235 -0
- package/src/integrations/state.ts +290 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer.ts +492 -0
- package/src/lib/bounded-body.ts +46 -8
- package/src/lib/bun-runtime.ts +110 -1
- package/src/lib/bun-stream-caps.ts +2 -1
- package/src/lib/redact.ts +407 -2
- package/src/lib/shadow-call.ts +24 -0
- package/src/lib/translator-budget.ts +10 -0
- package/src/lib/upstream-reachability.ts +91 -0
- package/src/lib/upstream-retry.ts +154 -2
- package/src/lib/windows-secret-acl.ts +212 -11
- package/src/lib/winsw.ts +9 -3
- package/src/oauth/index.ts +61 -3
- package/src/oauth/key-providers.ts +4 -0
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/store.ts +31 -6
- package/src/oauth/token-guardian.ts +9 -3
- package/src/providers/codex-capacity.ts +288 -0
- package/src/providers/derive.ts +33 -1
- package/src/providers/free-directory.ts +3 -1
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/openai-sidecar.ts +64 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota.ts +334 -26
- package/src/providers/registry.ts +284 -16
- package/src/providers/xai-transport.ts +11 -4
- package/src/responses/compaction.ts +8 -1
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/parser.ts +44 -2
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +105 -0
- package/src/responses/spill-store.ts +45 -8
- package/src/responses/state.ts +161 -17
- package/src/router.ts +302 -16
- package/src/routing/analytics.ts +377 -0
- package/src/routing/capability.ts +204 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +444 -0
- package/src/routing/health.ts +401 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +590 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile.ts +423 -0
- package/src/routing/quota.ts +124 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +686 -0
- package/src/server/auth-cors.ts +46 -6
- package/src/server/chat-completions.ts +28 -13
- package/src/server/claude-messages.ts +23 -15
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +10 -1
- package/src/server/index.ts +238 -52
- package/src/server/lifecycle.ts +228 -9
- package/src/server/live.ts +14 -3
- package/src/server/management/agent-settings-routes.ts +64 -14
- package/src/server/management/combo-routes.ts +5 -5
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +42 -2
- package/src/server/management/integration-routes.ts +538 -0
- package/src/server/management/logs-usage-routes.ts +1 -1
- package/src/server/management/model-routes.ts +32 -113
- package/src/server/management/model-rows.ts +117 -0
- package/src/server/management/native-integration-routes.ts +587 -0
- package/src/server/management/oauth-account-routes.ts +1 -1
- package/src/server/management/provider-routes.ts +218 -117
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +177 -0
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +276 -24
- package/src/server/management/system-routes.ts +4 -0
- package/src/server/management-api.ts +84 -9
- package/src/server/management-auth.ts +43 -5
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +120 -6
- package/src/server/request-log.ts +26 -6
- package/src/server/responses/collaboration.ts +63 -8
- package/src/server/responses/compact.ts +272 -41
- package/src/server/responses/core.ts +730 -132
- package/src/server/responses/fetch-helpers.ts +15 -1
- package/src/server/responses-item-id-repair.ts +32 -3
- package/src/server/responses-json-events.ts +52 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/search.ts +51 -6
- package/src/server/sse-payload-rewrite.ts +89 -12
- package/src/server/startup-health-cache.ts +7 -1
- package/src/server/ws-bridge.ts +11 -17
- package/src/service-manager-probe.ts +297 -0
- package/src/service.ts +222 -32
- package/src/tray/windows-tray.ps1 +9 -0
- package/src/tray/windows.ts +15 -7
- package/src/types.ts +194 -14
- package/src/update/index.ts +13 -13
- package/src/update/job.ts +24 -21
- package/src/update/notify.ts +7 -3
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +129 -10
- package/src/usage/log.ts +50 -15
- package/src/usage/summary.ts +4 -4
- package/src/vision/index.ts +6 -1
- package/src/web-search/loop.ts +161 -34
- package/gui/dist/assets/index-OY43ubAq.css +0 -1
- package/gui/dist/assets/index-YwNnKZcL.js +0 -67
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
providerBaseUrlConfigError,
|
|
12
12
|
providerHeadersConfigError,
|
|
13
13
|
saveConfigPreservingClaudeCode,
|
|
14
|
+
withConfigMutationLockSync,
|
|
14
15
|
} from "../../config";
|
|
15
16
|
import {
|
|
16
17
|
clearLoginState,
|
|
@@ -27,7 +28,7 @@ import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
|
|
|
27
28
|
import { ProviderOutboundPolicyError, providerOutboundGet, providerRedirectError } from "../../lib/provider-outbound";
|
|
28
29
|
import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
|
|
29
30
|
import { deriveProviderPresets } from "../../providers/derive";
|
|
30
|
-
import { providerCodexAccountMode } from "../../providers/registry";
|
|
31
|
+
import { providerCodexAccountMode, providerMatchesRegistryTransport } from "../../providers/registry";
|
|
31
32
|
import {
|
|
32
33
|
extractModelEnvelopeRows,
|
|
33
34
|
extractProviderModelItems,
|
|
@@ -70,8 +71,169 @@ import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, C
|
|
|
70
71
|
import type { ManagementContext } from "./context";
|
|
71
72
|
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
72
73
|
|
|
74
|
+
type ProviderPatchApplication =
|
|
75
|
+
| { error: string }
|
|
76
|
+
| {
|
|
77
|
+
next: OcxProviderConfig;
|
|
78
|
+
touched: boolean;
|
|
79
|
+
editorTouched: boolean;
|
|
80
|
+
enablingOpenAi: boolean;
|
|
81
|
+
headersTouched: boolean;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Apply the recognized PATCH field mask onto a provider copy. The caller runs this once
|
|
86
|
+
* for validation and again inside the config mutation lock against the newest provider,
|
|
87
|
+
* so a concurrent PATCH cannot be erased by a save of a stale snapshot. Only synchronous
|
|
88
|
+
* checks live here; the async destination probe stays in the route.
|
|
89
|
+
*/
|
|
90
|
+
function applyProviderPatchFields(
|
|
91
|
+
name: string,
|
|
92
|
+
provider: OcxProviderConfig,
|
|
93
|
+
rawBody: Record<string, unknown>,
|
|
94
|
+
keys: string[],
|
|
95
|
+
config: OcxConfig,
|
|
96
|
+
): ProviderPatchApplication {
|
|
97
|
+
const next: OcxProviderConfig = { ...provider };
|
|
98
|
+
let touched = false;
|
|
99
|
+
let headersTouched = false;
|
|
100
|
+
|
|
101
|
+
if (Object.hasOwn(rawBody, "disabled")) {
|
|
102
|
+
if (typeof rawBody.disabled !== "boolean") return { error: "disabled must be a boolean" };
|
|
103
|
+
if (rawBody.disabled && name === config.defaultProvider) {
|
|
104
|
+
return { error: "cannot disable the default provider; set another default first" };
|
|
105
|
+
}
|
|
106
|
+
next.disabled = rawBody.disabled;
|
|
107
|
+
touched = true;
|
|
108
|
+
}
|
|
109
|
+
if (Object.hasOwn(rawBody, "adapter")) {
|
|
110
|
+
if (typeof rawBody.adapter !== "string" || !rawBody.adapter.trim()) return { error: "adapter must be a non-empty string" };
|
|
111
|
+
next.adapter = rawBody.adapter.trim();
|
|
112
|
+
touched = true;
|
|
113
|
+
}
|
|
114
|
+
if (Object.hasOwn(rawBody, "baseUrl")) {
|
|
115
|
+
if (typeof rawBody.baseUrl !== "string" || !rawBody.baseUrl.trim()) return { error: "baseUrl must be a non-empty string" };
|
|
116
|
+
next.baseUrl = rawBody.baseUrl.trim();
|
|
117
|
+
touched = true;
|
|
118
|
+
}
|
|
119
|
+
if (Object.hasOwn(rawBody, "defaultModel")) {
|
|
120
|
+
if (typeof rawBody.defaultModel !== "string") return { error: "defaultModel must be a string" };
|
|
121
|
+
const dm = rawBody.defaultModel.trim();
|
|
122
|
+
if (dm) next.defaultModel = dm;
|
|
123
|
+
else delete next.defaultModel;
|
|
124
|
+
touched = true;
|
|
125
|
+
}
|
|
126
|
+
if (Object.hasOwn(rawBody, "authMode")) {
|
|
127
|
+
if (typeof rawBody.authMode !== "string") return { error: "authMode must be a string" };
|
|
128
|
+
const mode = rawBody.authMode.trim();
|
|
129
|
+
if (mode === "key" || mode === "forward" || mode === "oauth" || mode === "local") {
|
|
130
|
+
next.authMode = mode;
|
|
131
|
+
touched = true;
|
|
132
|
+
} else if (mode === "") {
|
|
133
|
+
delete next.authMode;
|
|
134
|
+
touched = true;
|
|
135
|
+
} else {
|
|
136
|
+
return { error: "authMode must be key, forward, oauth, or local" };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (Object.hasOwn(rawBody, "apiKeyTransport")) {
|
|
140
|
+
const transport = rawBody.apiKeyTransport;
|
|
141
|
+
if (transport === "x-api-key" || transport === "bearer") {
|
|
142
|
+
next.apiKeyTransport = transport;
|
|
143
|
+
touched = true;
|
|
144
|
+
} else if (transport === "") {
|
|
145
|
+
delete next.apiKeyTransport;
|
|
146
|
+
touched = true;
|
|
147
|
+
} else {
|
|
148
|
+
return { error: "apiKeyTransport must be x-api-key, bearer, or empty to clear" };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (Object.hasOwn(rawBody, "note")) {
|
|
152
|
+
if (typeof rawBody.note !== "string") return { error: "note must be a string" };
|
|
153
|
+
const note = rawBody.note.trim();
|
|
154
|
+
if (note) next.note = note;
|
|
155
|
+
else delete next.note;
|
|
156
|
+
touched = true;
|
|
157
|
+
}
|
|
158
|
+
if (Object.hasOwn(rawBody, "allowPrivateNetwork")) {
|
|
159
|
+
if (typeof rawBody.allowPrivateNetwork !== "boolean") return { error: "allowPrivateNetwork must be a boolean" };
|
|
160
|
+
next.allowPrivateNetwork = rawBody.allowPrivateNetwork;
|
|
161
|
+
touched = true;
|
|
162
|
+
}
|
|
163
|
+
if (Object.hasOwn(rawBody, "liveModels")) {
|
|
164
|
+
if (typeof rawBody.liveModels !== "boolean") return { error: "liveModels must be a boolean" };
|
|
165
|
+
next.liveModels = rawBody.liveModels;
|
|
166
|
+
touched = true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// headers is the one object-valued field in the mask. PATCH semantics merge it
|
|
170
|
+
// shallowly into the existing block so a single fingerprint header can be added
|
|
171
|
+
// without wiping the rest; null or an empty object clears user-managed headers.
|
|
172
|
+
if (Object.hasOwn(rawBody, "headers")) {
|
|
173
|
+
const headersValue = rawBody.headers;
|
|
174
|
+
if (headersValue === null || (isPlainRecord(headersValue) && Object.keys(headersValue).length === 0)) {
|
|
175
|
+
// Registry-owned static metadata (e.g. opencode-free's x-opencode-client marker)
|
|
176
|
+
// is not user-managed: restoring it keeps the upstream transport intact after a
|
|
177
|
+
// clear instead of deleting the whole block.
|
|
178
|
+
const entry = getProviderRegistryEntry(name);
|
|
179
|
+
if (entry?.staticHeaders && providerMatchesRegistryTransport(name, next)) {
|
|
180
|
+
next.headers = { ...entry.staticHeaders };
|
|
181
|
+
} else {
|
|
182
|
+
delete next.headers;
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
if (!isPlainRecord(headersValue)) return { error: "headers must be an object" };
|
|
186
|
+
const headersError = providerHeadersConfigError(headersValue);
|
|
187
|
+
if (headersError) return { error: headersError };
|
|
188
|
+
// Header names are case-insensitive on the wire. Drop any existing key whose
|
|
189
|
+
// lowercase name collides with an incoming one, or Headers normalization would
|
|
190
|
+
// send a combined "x-custom: v1, v2" value upstream.
|
|
191
|
+
const incoming = new Map(
|
|
192
|
+
Object.entries(headersValue as Record<string, string>).map(([key, value]) => [key.toLowerCase(), [key, value] as const]),
|
|
193
|
+
);
|
|
194
|
+
const merged: Record<string, string> = {};
|
|
195
|
+
for (const [key, value] of Object.entries(next.headers ?? {})) {
|
|
196
|
+
if (!incoming.has(key.toLowerCase())) merged[key] = value;
|
|
197
|
+
}
|
|
198
|
+
for (const [key, value] of incoming.values()) merged[key] = value;
|
|
199
|
+
next.headers = merged;
|
|
200
|
+
}
|
|
201
|
+
touched = true;
|
|
202
|
+
headersTouched = true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!touched) return { error: "no recognized fields to update" };
|
|
206
|
+
|
|
207
|
+
// A disabled-only toggle preserves the v2 fast lane for non-openai providers: it changes
|
|
208
|
+
// routing eligibility, not the provider shape. Re-enabling `openai` is different — a
|
|
209
|
+
// malformed disabled row must not come back online unchanged, so canonicalize/reject
|
|
210
|
+
// against the same built-in gate used by mode PATCH and POST.
|
|
211
|
+
const editorTouched = keys.some(key => key !== "disabled");
|
|
212
|
+
const enablingOpenAi = name === "openai"
|
|
213
|
+
&& Object.hasOwn(rawBody, "disabled")
|
|
214
|
+
&& rawBody.disabled === false
|
|
215
|
+
&& provider.disabled === true;
|
|
216
|
+
if (!editorTouched && enablingOpenAi) {
|
|
217
|
+
if (!isCanonicalOpenAiForwardProvider(next)) {
|
|
218
|
+
return { error: "provider openai must be the canonical built-in provider" };
|
|
219
|
+
}
|
|
220
|
+
// Persist the byte-identical canonical URL so config.ts startup checks (case-sensitive)
|
|
221
|
+
// accept the row after we fill mode. Equivalent hosts like CHATGPT.com/:443 normalize here.
|
|
222
|
+
next.baseUrl = CODEX_FORWARD_BASE_URL;
|
|
223
|
+
// Fill missing mode so a disabled canonical row becomes a complete live openai entry.
|
|
224
|
+
if (next.codexAccountMode !== "pool" && next.codexAccountMode !== "direct") {
|
|
225
|
+
next.codexAccountMode = "pool";
|
|
226
|
+
}
|
|
227
|
+
if (next.disabled === false) delete next.disabled;
|
|
228
|
+
// Canonical openai never uses private-network opt-in; drop a stale flag that
|
|
229
|
+
// was ignored for the DNS probe so it cannot linger on the live row.
|
|
230
|
+
delete next.allowPrivateNetwork;
|
|
231
|
+
}
|
|
232
|
+
return { next, touched, editorTouched, enablingOpenAi, headersTouched };
|
|
233
|
+
}
|
|
234
|
+
|
|
73
235
|
export async function handleProviderRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
74
|
-
const { req, url, config, deps,
|
|
236
|
+
const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
|
|
75
237
|
|
|
76
238
|
if (url.pathname === "/api/provider-quotas" && req.method === "GET") {
|
|
77
239
|
const forceRefresh = url.searchParams.get("refresh") === "1" || url.searchParams.get("refresh") === "true";
|
|
@@ -82,6 +244,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
82
244
|
return jsonResponse(Object.entries(config.providers).map(([name, p]) => ({
|
|
83
245
|
name, adapter: p.adapter, baseUrl: publicProviderBaseUrl(p.baseUrl), defaultModel: p.defaultModel,
|
|
84
246
|
hasApiKey: !!p.apiKey,
|
|
247
|
+
// Presence only (#959 review): header names and values never leave the process.
|
|
248
|
+
hasHeaders: !!p.headers && Object.keys(p.headers).length > 0,
|
|
85
249
|
allowPrivateNetwork: p.allowPrivateNetwork === true,
|
|
86
250
|
liveModels: p.liveModels !== false,
|
|
87
251
|
models: p.models ?? [],
|
|
@@ -144,8 +308,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
144
308
|
}
|
|
145
309
|
const { clearModelCache } = await import("../../codex/model-cache");
|
|
146
310
|
clearModelCache(name);
|
|
147
|
-
await
|
|
148
|
-
return jsonResponse({ success: true, name });
|
|
311
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
312
|
+
return jsonResponse({ success: true, name, catalogRefresh });
|
|
149
313
|
}
|
|
150
314
|
|
|
151
315
|
if (url.pathname === "/api/providers" && req.method === "PATCH") {
|
|
@@ -213,106 +377,16 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
213
377
|
if (Object.hasOwn(rawBody, "apiKey")) {
|
|
214
378
|
return jsonResponse({ error: "apiKey cannot be patched here; use the provider API-key endpoints" }, 400);
|
|
215
379
|
}
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (Object.hasOwn(rawBody, "disabled")) {
|
|
220
|
-
if (typeof rawBody.disabled !== "boolean") return jsonResponse({ error: "disabled must be a boolean" }, 400);
|
|
221
|
-
if (rawBody.disabled && name === config.defaultProvider) {
|
|
222
|
-
return jsonResponse({ error: "cannot disable the default provider; set another default first" }, 400);
|
|
223
|
-
}
|
|
224
|
-
next.disabled = rawBody.disabled;
|
|
225
|
-
touched = true;
|
|
226
|
-
}
|
|
227
|
-
if (Object.hasOwn(rawBody, "adapter")) {
|
|
228
|
-
if (typeof rawBody.adapter !== "string" || !rawBody.adapter.trim()) return jsonResponse({ error: "adapter must be a non-empty string" }, 400);
|
|
229
|
-
next.adapter = rawBody.adapter.trim();
|
|
230
|
-
touched = true;
|
|
231
|
-
}
|
|
232
|
-
if (Object.hasOwn(rawBody, "baseUrl")) {
|
|
233
|
-
if (typeof rawBody.baseUrl !== "string" || !rawBody.baseUrl.trim()) return jsonResponse({ error: "baseUrl must be a non-empty string" }, 400);
|
|
234
|
-
next.baseUrl = rawBody.baseUrl.trim();
|
|
235
|
-
touched = true;
|
|
236
|
-
}
|
|
237
|
-
if (Object.hasOwn(rawBody, "defaultModel")) {
|
|
238
|
-
if (typeof rawBody.defaultModel !== "string") return jsonResponse({ error: "defaultModel must be a string" }, 400);
|
|
239
|
-
const dm = rawBody.defaultModel.trim();
|
|
240
|
-
if (dm) next.defaultModel = dm;
|
|
241
|
-
else delete next.defaultModel;
|
|
242
|
-
touched = true;
|
|
243
|
-
}
|
|
244
|
-
if (Object.hasOwn(rawBody, "authMode")) {
|
|
245
|
-
if (typeof rawBody.authMode !== "string") return jsonResponse({ error: "authMode must be a string" }, 400);
|
|
246
|
-
const mode = rawBody.authMode.trim();
|
|
247
|
-
if (mode === "key" || mode === "forward" || mode === "oauth" || mode === "local") {
|
|
248
|
-
next.authMode = mode;
|
|
249
|
-
touched = true;
|
|
250
|
-
} else if (mode === "") {
|
|
251
|
-
delete next.authMode;
|
|
252
|
-
touched = true;
|
|
253
|
-
} else {
|
|
254
|
-
return jsonResponse({ error: "authMode must be key, forward, oauth, or local" }, 400);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
if (Object.hasOwn(rawBody, "apiKeyTransport")) {
|
|
258
|
-
const transport = rawBody.apiKeyTransport;
|
|
259
|
-
if (transport === "x-api-key" || transport === "bearer") {
|
|
260
|
-
next.apiKeyTransport = transport;
|
|
261
|
-
touched = true;
|
|
262
|
-
} else if (transport === "") {
|
|
263
|
-
delete next.apiKeyTransport;
|
|
264
|
-
touched = true;
|
|
265
|
-
} else {
|
|
266
|
-
return jsonResponse({ error: "apiKeyTransport must be x-api-key, bearer, or empty to clear" }, 400);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
if (Object.hasOwn(rawBody, "note")) {
|
|
270
|
-
if (typeof rawBody.note !== "string") return jsonResponse({ error: "note must be a string" }, 400);
|
|
271
|
-
const note = rawBody.note.trim();
|
|
272
|
-
if (note) next.note = note;
|
|
273
|
-
else delete next.note;
|
|
274
|
-
touched = true;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (Object.hasOwn(rawBody, "allowPrivateNetwork")) {
|
|
278
|
-
if (typeof rawBody.allowPrivateNetwork !== "boolean") return jsonResponse({ error: "allowPrivateNetwork must be a boolean" }, 400);
|
|
279
|
-
next.allowPrivateNetwork = rawBody.allowPrivateNetwork;
|
|
280
|
-
touched = true;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if (Object.hasOwn(rawBody, "liveModels")) {
|
|
284
|
-
if (typeof rawBody.liveModels !== "boolean") return jsonResponse({ error: "liveModels must be a boolean" }, 400);
|
|
285
|
-
next.liveModels = rawBody.liveModels;
|
|
286
|
-
touched = true;
|
|
287
|
-
}
|
|
380
|
+
const applied = applyProviderPatchFields(name, config.providers[name]!, rawBody, keys, config);
|
|
381
|
+
if ("error" in applied) return jsonResponse({ error: applied.error }, 400);
|
|
382
|
+
const next = applied.next;
|
|
288
383
|
|
|
289
|
-
if (
|
|
290
|
-
|
|
291
|
-
// A disabled-only toggle preserves the v2 fast lane for non-openai providers: it changes
|
|
292
|
-
// routing eligibility, not the provider shape. Re-enabling `openai` is different — a
|
|
293
|
-
// malformed disabled row must not come back online unchanged, so canonicalize/reject
|
|
294
|
-
// against the same built-in gate used by mode PATCH and POST.
|
|
295
|
-
const editorTouched = keys.some(key => key !== "disabled");
|
|
296
|
-
const enablingOpenAi = name === "openai"
|
|
297
|
-
&& Object.hasOwn(rawBody, "disabled")
|
|
298
|
-
&& rawBody.disabled === false
|
|
299
|
-
&& config.providers[name]?.disabled === true;
|
|
300
|
-
if (editorTouched) {
|
|
384
|
+
if (applied.editorTouched) {
|
|
301
385
|
const providerError = providerManagementConfigError(name, next);
|
|
302
386
|
if (providerError) return jsonResponse({ error: providerError }, 400);
|
|
303
387
|
const resolvedError = await providerDestinationResolvedError(name, next);
|
|
304
388
|
if (resolvedError) return jsonResponse({ error: resolvedError }, 400);
|
|
305
|
-
} else if (enablingOpenAi) {
|
|
306
|
-
if (!isCanonicalOpenAiForwardProvider(next)) {
|
|
307
|
-
return jsonResponse({ error: "provider openai must be the canonical built-in provider" }, 400);
|
|
308
|
-
}
|
|
309
|
-
// Persist the byte-identical canonical URL so config.ts startup checks (case-sensitive)
|
|
310
|
-
// accept the row after we fill mode. Equivalent hosts like CHATGPT.com/:443 normalize here.
|
|
311
|
-
next.baseUrl = CODEX_FORWARD_BASE_URL;
|
|
312
|
-
// Fill missing mode so a disabled canonical row becomes a complete live openai entry.
|
|
313
|
-
if (next.codexAccountMode !== "pool" && next.codexAccountMode !== "direct") {
|
|
314
|
-
next.codexAccountMode = "pool";
|
|
315
|
-
}
|
|
389
|
+
} else if (applied.enablingOpenAi) {
|
|
316
390
|
// Same DNS gate as POST: Clash fake-IP only. Never honor a persisted
|
|
317
391
|
// allowPrivateNetwork on this path — it must not bypass the built-in guard.
|
|
318
392
|
const resolvedError = await providerDestinationResolvedError(
|
|
@@ -321,26 +395,47 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
321
395
|
{ allowBenchmarkAddresses: true },
|
|
322
396
|
);
|
|
323
397
|
if (resolvedError) return jsonResponse({ error: resolvedError }, 400);
|
|
324
|
-
if (next.disabled === false) delete next.disabled;
|
|
325
|
-
// Canonical openai never uses private-network opt-in; drop a stale flag that
|
|
326
|
-
// was ignored for the DNS probe so it cannot linger on the live row.
|
|
327
|
-
delete next.allowPrivateNetwork;
|
|
328
398
|
}
|
|
329
399
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
400
|
+
// The live config is shared and the destination probe above is awaited. Re-apply the
|
|
401
|
+
// mask onto the newest provider under the mutation lock right before saving, so two
|
|
402
|
+
// concurrent PATCHes updating different fields/headers both survive instead of the
|
|
403
|
+
// later save clobbering the earlier snapshot.
|
|
404
|
+
let replayError: string | undefined;
|
|
405
|
+
withConfigMutationLockSync(() => {
|
|
406
|
+
const replay = applyProviderPatchFields(name, config.providers[name]!, rawBody, keys, config);
|
|
407
|
+
if ("error" in replay) {
|
|
408
|
+
replayError = replay.error;
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (replay.editorTouched) {
|
|
412
|
+
const syncError = providerManagementConfigError(name, replay.next);
|
|
413
|
+
if (syncError) {
|
|
414
|
+
replayError = syncError;
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
} else if (replay.enablingOpenAi && !isCanonicalOpenAiForwardProvider(replay.next)) {
|
|
418
|
+
replayError = "provider openai must be the canonical built-in provider";
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
// A PATCH that managed headers owns the resulting block: the clear path restores
|
|
422
|
+
// registry static headers, so exact-match stripping must not erase them again.
|
|
423
|
+
config.providers[name] = replay.headersTouched ? replay.next : stripRegistryOnlyStaticHeaders(name, replay.next);
|
|
424
|
+
saveConfigPreservingClaudeCode(config);
|
|
425
|
+
});
|
|
426
|
+
if (replayError !== undefined) return jsonResponse({ error: replayError }, 409);
|
|
333
427
|
reconcileLiveStateStores();
|
|
334
|
-
if (editorTouched) {
|
|
428
|
+
if (applied.editorTouched) {
|
|
335
429
|
const { clearModelCache } = await import("../../codex/model-cache");
|
|
336
430
|
clearModelCache(name);
|
|
337
431
|
}
|
|
338
|
-
await
|
|
432
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
339
433
|
return jsonResponse({
|
|
340
434
|
success: true,
|
|
341
435
|
name,
|
|
342
436
|
disabled: config.providers[name]!.disabled === true,
|
|
343
437
|
hasApiKey: !!config.providers[name]!.apiKey,
|
|
438
|
+
catalogRefresh,
|
|
344
439
|
});
|
|
345
440
|
}
|
|
346
441
|
|
|
@@ -484,8 +579,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
484
579
|
reconcileLiveStateStores();
|
|
485
580
|
const { clearModelCache: clearCache } = await import("../../codex/model-cache");
|
|
486
581
|
clearCache(name);
|
|
487
|
-
await
|
|
488
|
-
return jsonResponse({ success: true, ...(fallbackDefault ? { defaultProvider: fallbackDefault } : {}) });
|
|
582
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
583
|
+
return jsonResponse({ success: true, ...(fallbackDefault ? { defaultProvider: fallbackDefault } : {}), catalogRefresh });
|
|
489
584
|
}
|
|
490
585
|
|
|
491
586
|
if (url.pathname === "/api/provider-context-caps" && req.method === "GET") {
|
|
@@ -497,7 +592,13 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
497
592
|
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
498
593
|
const { saveConfigPreservingClaudeCode: save } = await import("../../config");
|
|
499
594
|
const { clearModelCache } = await import("../../codex/model-cache");
|
|
500
|
-
const respond = () => jsonResponse({
|
|
595
|
+
const respond = (catalogRefresh: Awaited<ReturnType<typeof convergeCodexCatalog>>) => jsonResponse({
|
|
596
|
+
ok: true,
|
|
597
|
+
cap: DEFAULT_PROVIDER_CONTEXT_CAP,
|
|
598
|
+
value: globalContextCapValue(config),
|
|
599
|
+
caps: providerContextCaps(config),
|
|
600
|
+
catalogRefresh,
|
|
601
|
+
});
|
|
501
602
|
|
|
502
603
|
// Branch 1: set the global cap value and re-point every enabled provider to it.
|
|
503
604
|
if (body.value !== undefined) {
|
|
@@ -509,8 +610,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
509
610
|
save(config);
|
|
510
611
|
reconcileLiveStateStores();
|
|
511
612
|
for (const provider of affected) clearModelCache(provider);
|
|
512
|
-
await
|
|
513
|
-
return respond();
|
|
613
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
614
|
+
return respond(catalogRefresh);
|
|
514
615
|
}
|
|
515
616
|
|
|
516
617
|
// Branch 2: enable/clear the cap for every provider at once.
|
|
@@ -524,8 +625,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
524
625
|
save(config);
|
|
525
626
|
reconcileLiveStateStores();
|
|
526
627
|
for (const provider of new Set([...before, ...names])) clearModelCache(provider);
|
|
527
|
-
await
|
|
528
|
-
return respond();
|
|
628
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
629
|
+
return respond(catalogRefresh);
|
|
529
630
|
}
|
|
530
631
|
|
|
531
632
|
// Branch 3: existing per-provider toggle (enable writes the current global value).
|
|
@@ -543,8 +644,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
543
644
|
save(config);
|
|
544
645
|
reconcileLiveStateStores();
|
|
545
646
|
clearModelCache(provider);
|
|
546
|
-
await
|
|
547
|
-
return respond();
|
|
647
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
648
|
+
return respond(catalogRefresh);
|
|
548
649
|
}
|
|
549
650
|
|
|
550
651
|
// Complete GUI picker presets, derived from the canonical provider registry. The GUI is a
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor-paginated request-history API (RI-02).
|
|
3
|
+
*
|
|
4
|
+
* - `GET /api/request-history` - keyset-paginated rows with filters
|
|
5
|
+
* - `GET /api/request-history/:requestId` - one canonical row
|
|
6
|
+
* - `GET /api/request-history/:requestId/route-decision` - why-this-route
|
|
7
|
+
* explanation (RI-09): durable trace + attempts + outcome
|
|
8
|
+
*
|
|
9
|
+
* The index is a derived projection of `usage.jsonl`; every response carries
|
|
10
|
+
* an `index` status block so callers can see schema version, indexed rows and
|
|
11
|
+
* any repair the indexer performed.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
queryRequestHistory,
|
|
16
|
+
requestHistoryRowById,
|
|
17
|
+
REQUEST_HISTORY_MAX_PAGE_SIZE,
|
|
18
|
+
} from "../../routing/history/indexer";
|
|
19
|
+
import { InvalidCursorError } from "../../routing/history/cursor";
|
|
20
|
+
import { requestLogEntryFromPersistedUsage } from "../request-log";
|
|
21
|
+
import { requestLogDto } from "./shared";
|
|
22
|
+
import { jsonResponse } from "../auth-cors";
|
|
23
|
+
import type { ManagementContext } from "./context";
|
|
24
|
+
import type { PersistedUsageEntry } from "../../usage/log";
|
|
25
|
+
|
|
26
|
+
function parseQueryInt(raw: string | null): number | undefined | "invalid" {
|
|
27
|
+
if (raw === null) return undefined;
|
|
28
|
+
const trimmed = raw.trim();
|
|
29
|
+
if (trimmed.length === 0) return "invalid";
|
|
30
|
+
const value = Number(trimmed);
|
|
31
|
+
return Number.isInteger(value) ? value : "invalid";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function finalAttemptTarget(entry: PersistedUsageEntry): { provider: string; model: string } {
|
|
35
|
+
const attempts = entry.attempts;
|
|
36
|
+
if (Array.isArray(attempts) && attempts.length > 0) {
|
|
37
|
+
const last = attempts[attempts.length - 1];
|
|
38
|
+
if (last && typeof last.provider === "string" && typeof last.model === "string") {
|
|
39
|
+
return { provider: last.provider, model: last.model };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { provider: entry.provider, model: entry.model };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function handleRequestHistoryRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
46
|
+
const { url, req, config } = ctx;
|
|
47
|
+
if (!url.pathname.startsWith("/api/request-history")) return null;
|
|
48
|
+
|
|
49
|
+
if (url.pathname === "/api/request-history" && req.method === "GET") {
|
|
50
|
+
const statusRaw = parseQueryInt(url.searchParams.get("status"));
|
|
51
|
+
if (statusRaw === "invalid") {
|
|
52
|
+
return jsonResponse({ error: { code: "invalid_status", message: "status must be an integer from 100 to 599" } }, 400, req, config);
|
|
53
|
+
}
|
|
54
|
+
const status = statusRaw;
|
|
55
|
+
if (status !== undefined && (status < 100 || status > 599)) {
|
|
56
|
+
return jsonResponse({ error: { code: "invalid_status", message: "status must be an integer from 100 to 599" } }, 400, req, config);
|
|
57
|
+
}
|
|
58
|
+
const fromRaw = parseQueryInt(url.searchParams.get("from"));
|
|
59
|
+
if (fromRaw === "invalid") {
|
|
60
|
+
return jsonResponse({ error: { code: "invalid_from", message: "from must be an integer timestamp" } }, 400, req, config);
|
|
61
|
+
}
|
|
62
|
+
const toRaw = parseQueryInt(url.searchParams.get("to"));
|
|
63
|
+
if (toRaw === "invalid") {
|
|
64
|
+
return jsonResponse({ error: { code: "invalid_to", message: "to must be an integer timestamp" } }, 400, req, config);
|
|
65
|
+
}
|
|
66
|
+
const from = fromRaw;
|
|
67
|
+
const to = toRaw;
|
|
68
|
+
if (from !== undefined && to !== undefined && from > to) {
|
|
69
|
+
return jsonResponse({ error: { code: "invalid_range", message: "from must not be after to" } }, 400, req, config);
|
|
70
|
+
}
|
|
71
|
+
const limitRaw = url.searchParams.get("limit");
|
|
72
|
+
const limitParsed = limitRaw === null ? undefined : parseQueryInt(limitRaw);
|
|
73
|
+
if (limitParsed === "invalid") {
|
|
74
|
+
return jsonResponse(
|
|
75
|
+
{ error: { code: "invalid_limit", message: `limit must be an integer from 1 to ${REQUEST_HISTORY_MAX_PAGE_SIZE}` } },
|
|
76
|
+
400,
|
|
77
|
+
req,
|
|
78
|
+
config,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
const limit = limitParsed;
|
|
82
|
+
if (limit !== undefined && (limit < 1 || limit > REQUEST_HISTORY_MAX_PAGE_SIZE)) {
|
|
83
|
+
return jsonResponse(
|
|
84
|
+
{ error: { code: "invalid_limit", message: `limit must be an integer from 1 to ${REQUEST_HISTORY_MAX_PAGE_SIZE}` } },
|
|
85
|
+
400,
|
|
86
|
+
req,
|
|
87
|
+
config,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
const cursor = url.searchParams.get("cursor");
|
|
91
|
+
try {
|
|
92
|
+
const page = await queryRequestHistory({
|
|
93
|
+
provider: url.searchParams.get("provider")?.trim() || undefined,
|
|
94
|
+
model: url.searchParams.get("model")?.trim() || undefined,
|
|
95
|
+
requestedModel: url.searchParams.get("requestedModel")?.trim() || undefined,
|
|
96
|
+
status,
|
|
97
|
+
conversationId: url.searchParams.get("conversationId")?.trim() || undefined,
|
|
98
|
+
surface: url.searchParams.get("surface")?.trim() || undefined,
|
|
99
|
+
inboundProtocol: url.searchParams.get("inboundProtocol")?.trim() || undefined,
|
|
100
|
+
apiKeyId: url.searchParams.get("apiKeyId")?.trim() || undefined,
|
|
101
|
+
profileId: url.searchParams.get("profileId")?.trim() || undefined,
|
|
102
|
+
fallback: url.searchParams.get("fallback") === "true"
|
|
103
|
+
? true
|
|
104
|
+
: url.searchParams.get("fallback") === "false" ? false : undefined,
|
|
105
|
+
from,
|
|
106
|
+
to,
|
|
107
|
+
}, cursor, limit);
|
|
108
|
+
return jsonResponse({
|
|
109
|
+
entries: page.rows.map(row => requestLogDto(requestLogEntryFromPersistedUsage(row))),
|
|
110
|
+
...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
|
|
111
|
+
hasMore: page.hasMore,
|
|
112
|
+
index: {
|
|
113
|
+
schemaVersion: page.meta.schemaVersion,
|
|
114
|
+
indexedRows: page.meta.indexedRows,
|
|
115
|
+
sourceSize: page.meta.sourceSize,
|
|
116
|
+
sourceMtimeMs: page.meta.sourceMtimeMs,
|
|
117
|
+
builtAtMs: page.meta.builtAtMs,
|
|
118
|
+
lastError: page.meta.lastError,
|
|
119
|
+
},
|
|
120
|
+
}, 200, req, config);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
if (err instanceof InvalidCursorError) {
|
|
123
|
+
return jsonResponse({ error: { code: "invalid_cursor", message: "invalid cursor" } }, 400, req, config);
|
|
124
|
+
}
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (url.pathname.startsWith("/api/request-history/") && req.method === "GET") {
|
|
130
|
+
// Why-this-route explanation (RI-09): trace + attempts + outcome.
|
|
131
|
+
if (url.pathname.endsWith("/route-decision")) {
|
|
132
|
+
let requestId: string;
|
|
133
|
+
try {
|
|
134
|
+
requestId = decodeURIComponent(
|
|
135
|
+
url.pathname.slice("/api/request-history/".length, -"/route-decision".length),
|
|
136
|
+
);
|
|
137
|
+
} catch {
|
|
138
|
+
return jsonResponse({ error: { code: "not_found", message: "unknown request" } }, 404, req, config);
|
|
139
|
+
}
|
|
140
|
+
if (!requestId || requestId.includes("/")) {
|
|
141
|
+
return jsonResponse({ error: { code: "not_found", message: "unknown request" } }, 404, req, config);
|
|
142
|
+
}
|
|
143
|
+
const entry = await requestHistoryRowById(requestId);
|
|
144
|
+
if (!entry) {
|
|
145
|
+
return jsonResponse({ error: { code: "not_found", message: "unknown request" } }, 404, req, config);
|
|
146
|
+
}
|
|
147
|
+
const trace = entry.routeDecision ?? null;
|
|
148
|
+
const final = finalAttemptTarget(entry);
|
|
149
|
+
return jsonResponse({
|
|
150
|
+
requestId,
|
|
151
|
+
routeDecision: trace,
|
|
152
|
+
attemptSequence: entry.attempts ?? [],
|
|
153
|
+
outcome: {
|
|
154
|
+
status: entry.status,
|
|
155
|
+
...(entry.terminalStatus ? { terminalStatus: entry.terminalStatus } : {}),
|
|
156
|
+
...(entry.closeReason ? { closeReason: entry.closeReason } : {}),
|
|
157
|
+
...(entry.errorCode ? { errorCode: entry.errorCode } : {}),
|
|
158
|
+
...(entry.durationMs !== undefined ? { durationMs: entry.durationMs } : {}),
|
|
159
|
+
...(entry.usageStatus ? { usageStatus: entry.usageStatus } : {}),
|
|
160
|
+
},
|
|
161
|
+
summary: {
|
|
162
|
+
requestedModel: entry.requestedModel ?? entry.model,
|
|
163
|
+
routeKind: trace?.routeKind ?? null,
|
|
164
|
+
...(trace?.profile ? { profileId: trace.profile.id, revision: trace.profile.revision } : {}),
|
|
165
|
+
selected: trace?.selected
|
|
166
|
+
? { provider: trace.selected.provider, model: trace.selected.model }
|
|
167
|
+
: null,
|
|
168
|
+
finalProvider: final.provider,
|
|
169
|
+
finalModel: final.model,
|
|
170
|
+
},
|
|
171
|
+
}, 200, req, config);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let requestId: string;
|
|
175
|
+
try {
|
|
176
|
+
requestId = decodeURIComponent(url.pathname.slice("/api/request-history/".length));
|
|
177
|
+
} catch {
|
|
178
|
+
return jsonResponse({ error: { code: "not_found", message: "unknown request" } }, 404, req, config);
|
|
179
|
+
}
|
|
180
|
+
if (!requestId || requestId.includes("/")) {
|
|
181
|
+
return jsonResponse({ error: { code: "not_found", message: "unknown request" } }, 404, req, config);
|
|
182
|
+
}
|
|
183
|
+
const entry = await requestHistoryRowById(requestId);
|
|
184
|
+
if (!entry) {
|
|
185
|
+
return jsonResponse({ error: { code: "not_found", message: "unknown request" } }, 404, req, config);
|
|
186
|
+
}
|
|
187
|
+
return jsonResponse(requestLogDto(requestLogEntryFromPersistedUsage(entry)), 200, req, config);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return null;
|
|
191
|
+
}
|