mixdog 0.8.0 → 0.9.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 +47 -23
- package/package.json +33 -27
- package/scripts/_test-folder-dialog.mjs +30 -0
- package/scripts/agent-parallel-smoke.mjs +388 -0
- package/scripts/agent-tag-reuse-smoke.mjs +183 -0
- package/scripts/background-task-meta-smoke.mjs +38 -0
- package/scripts/boot-smoke.mjs +52 -9
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/compact-active-turn-test.mjs +68 -0
- package/scripts/compact-smoke.mjs +859 -129
- package/scripts/compact-trigger-migration-smoke.mjs +187 -0
- package/scripts/fix-brief-fn.mjs +35 -0
- package/scripts/fix-format-tool-surface.mjs +24 -0
- package/scripts/fix-tool-exec-visible.mjs +42 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/hook-bus-test.mjs +330 -0
- package/scripts/lead-workflow-smoke.mjs +33 -39
- package/scripts/live-worker-smoke.mjs +43 -37
- package/scripts/llm-trace-summary.mjs +315 -0
- package/scripts/memory-meta-concurrency-test.mjs +20 -0
- package/scripts/output-style-smoke.mjs +56 -15
- package/scripts/parent-abort-link-test.mjs +44 -0
- package/scripts/patch-agent-brief.mjs +48 -0
- package/scripts/patch-app.mjs +21 -0
- package/scripts/patch-app2.mjs +18 -0
- package/scripts/patch-dist-brief.mjs +96 -0
- package/scripts/patch-tool-exec.mjs +70 -0
- package/scripts/pretool-ask-runtime-test.mjs +54 -0
- package/scripts/provider-toolcall-test.mjs +376 -0
- package/scripts/reactive-compact-persist-smoke.mjs +124 -0
- package/scripts/sanitize-tool-pairs-test.mjs +260 -0
- package/scripts/session-context-bench.mjs +344 -0
- package/scripts/session-ingest-smoke.mjs +177 -0
- package/scripts/set-effort-config-test.mjs +41 -0
- package/scripts/smoke-runtime-negative.ps1 +106 -0
- package/scripts/smoke-runtime-negative.sh +97 -0
- package/scripts/smoke.mjs +25 -0
- package/scripts/tool-result-hook-test.mjs +48 -0
- package/scripts/tool-smoke.mjs +1223 -95
- package/scripts/toolcall-args-test.mjs +150 -0
- package/scripts/tui-background-failure-smoke.mjs +73 -0
- package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
- package/src/agents/debugger/AGENT.md +8 -0
- package/src/agents/explore/AGENT.md +4 -0
- package/src/agents/heavy-worker/AGENT.md +9 -3
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +8 -0
- package/src/agents/scheduler-task/AGENT.md +12 -0
- package/src/agents/scheduler-task/agent.json +6 -0
- package/src/agents/webhook-handler/AGENT.md +12 -0
- package/src/agents/webhook-handler/agent.json +6 -0
- package/src/agents/worker/AGENT.md +9 -3
- package/src/app.mjs +77 -3
- package/src/defaults/hidden-roles.json +17 -12
- package/src/headless-role.mjs +117 -0
- package/src/help.mjs +30 -0
- package/src/hooks/lib/permission-evaluator.cjs +11 -475
- package/src/lib/keychain-cjs.cjs +9 -1
- package/src/lib/mixdog-debug.cjs +0 -7
- package/src/lib/rules-builder.cjs +240 -96
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2325 -450
- package/src/output-styles/default.md +12 -28
- package/src/output-styles/extreme-simple.md +9 -6
- package/src/output-styles/simple.md +22 -9
- package/src/repl.mjs +118 -59
- package/src/rules/agent/00-common.md +15 -0
- package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
- package/src/rules/agent/30-explorer.md +22 -0
- package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
- package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
- package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
- package/src/rules/lead/01-general.md +9 -5
- package/src/rules/lead/04-workflow.md +51 -12
- package/src/rules/lead/lead-tool.md +6 -0
- package/src/rules/shared/01-tool.md +12 -1
- package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
- package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
- package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
- package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
- package/src/runtime/agent/orchestrator/config.mjs +174 -55
- package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
- package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
- package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
- package/src/runtime/agent/orchestrator/session/loop.mjs +851 -674
- package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
- package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
- package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
- package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
- package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
- package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
- package/src/runtime/channels/index.mjs +152 -24
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -2
- package/src/runtime/channels/lib/tool-format.mjs +0 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/memory/index.mjs +635 -107
- package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
- package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
- package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
- package/src/runtime/memory/lib/memory-cycle1.mjs +38 -17
- package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
- package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
- package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
- package/src/runtime/memory/lib/memory.mjs +88 -9
- package/src/runtime/memory/lib/model-profile.mjs +1 -1
- package/src/runtime/memory/lib/pg/adapter.mjs +15 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
- package/src/runtime/memory/lib/session-ingest.mjs +194 -0
- package/src/runtime/memory/lib/trace-store.mjs +96 -51
- package/src/runtime/memory/tool-defs.mjs +46 -37
- package/src/runtime/search/index.mjs +102 -466
- package/src/runtime/search/lib/web-tools.mjs +45 -25
- package/src/runtime/search/tool-defs.mjs +16 -23
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +4 -3
- package/src/runtime/shared/background-tasks.mjs +122 -11
- package/src/runtime/shared/child-spawn-gate.mjs +145 -0
- package/src/runtime/shared/config.mjs +7 -4
- package/src/runtime/shared/err-text.mjs +131 -4
- package/src/runtime/shared/llm/cost.mjs +2 -2
- package/src/runtime/shared/llm/http-agent.mjs +23 -7
- package/src/runtime/shared/llm/index.mjs +34 -11
- package/src/runtime/shared/llm/usage-log.mjs +4 -4
- package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
- package/src/runtime/shared/singleton-owner.mjs +104 -0
- package/src/runtime/shared/tool-execution-contract.mjs +199 -20
- package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
- package/src/runtime/shared/tool-surface.mjs +624 -98
- package/src/runtime/shared/user-data-guard.mjs +0 -2
- package/src/standalone/agent-task-status.mjs +203 -0
- package/src/standalone/agent-task-status.test.mjs +76 -0
- package/src/standalone/agent-tool.mjs +1913 -0
- package/src/standalone/channel-worker.mjs +370 -14
- package/src/standalone/explore-tool.mjs +165 -70
- package/src/standalone/folder-dialog.mjs +314 -0
- package/src/standalone/hook-bus.mjs +898 -22
- package/src/standalone/memory-runtime-proxy.mjs +320 -0
- package/src/standalone/projects.mjs +226 -0
- package/src/standalone/provider-admin.mjs +41 -24
- package/src/standalone/seeds.mjs +2 -69
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4798 -2153
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +87 -29
- package/src/tui/components/Markdown.jsx +43 -77
- package/src/tui/components/MarkdownTable.jsx +9 -184
- package/src/tui/components/Message.jsx +28 -11
- package/src/tui/components/Picker.jsx +95 -56
- package/src/tui/components/PromptInput.jsx +367 -239
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +27 -21
- package/src/tui/components/Spinner.jsx +67 -38
- package/src/tui/components/StatusLine.jsx +606 -38
- package/src/tui/components/TextEntryPanel.jsx +128 -9
- package/src/tui/components/ToolExecution.jsx +617 -368
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +365 -0
- package/src/tui/components/tool-output-format.test.mjs +220 -0
- package/src/tui/dist/index.mjs +8915 -2418
- package/src/tui/engine-runtime-notification.test.mjs +115 -0
- package/src/tui/engine-tool-result-text.test.mjs +75 -0
- package/src/tui/engine.mjs +1455 -279
- package/src/tui/figures.mjs +21 -40
- package/src/tui/index.jsx +75 -31
- package/src/tui/input-editing.mjs +25 -0
- package/src/tui/markdown/format-token.mjs +511 -68
- package/src/tui/markdown/format-token.test.mjs +216 -0
- package/src/tui/markdown/render-ansi.mjs +94 -0
- package/src/tui/markdown/render-ansi.test.mjs +108 -0
- package/src/tui/markdown/stream-fence.mjs +34 -0
- package/src/tui/markdown/stream-fence.test.mjs +26 -0
- package/src/tui/markdown/table-layout.mjs +250 -0
- package/src/tui/paste-attachments.mjs +0 -7
- package/src/tui/spinner-verbs.mjs +1 -2
- package/src/tui/statusline-ansi-bridge.mjs +172 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
- package/src/tui/theme.mjs +746 -24
- package/src/tui/time-format.mjs +1 -1
- package/src/tui/transcript-tool-failures.mjs +67 -0
- package/src/tui/transcript-tool-failures.test.mjs +111 -0
- package/src/ui/ansi.mjs +1 -2
- package/src/ui/markdown.mjs +85 -26
- package/src/ui/markdown.test.mjs +70 -0
- package/src/ui/model-display.mjs +121 -0
- package/src/ui/session-stats.mjs +44 -0
- package/src/ui/statusline-context-label.test.mjs +15 -0
- package/src/ui/statusline.mjs +386 -178
- package/src/ui/tool-card.mjs +3 -16
- package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
- package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
- package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
- package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
- package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
- package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +12 -5
- package/src/workflows/default/workflow.json +0 -1
- package/src/workflows/solo/WORKFLOW.md +15 -0
- package/src/workflows/solo/workflow.json +7 -0
- package/vendor/ink/build/output.js +6 -1
- package/src/agents/scheduler-task.md +0 -3
- package/src/agents/web-researcher/AGENT.md +0 -3
- package/src/agents/web-researcher/agent.json +0 -6
- package/src/agents/webhook-handler.md +0 -3
- package/src/rules/bridge/00-common.md +0 -5
- package/src/rules/bridge/30-explorer.md +0 -4
- package/src/rules/lead/00-tool-lead.md +0 -5
- package/src/rules/shared/00-language.md +0 -3
- package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
- package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
- package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
- package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
- package/src/standalone/bridge-tool.mjs +0 -1414
- package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OpenAI ChatGPT OAuth
|
|
2
|
+
* OpenAI ChatGPT OAuth subscription provider.
|
|
3
3
|
*
|
|
4
4
|
* Dispatches over the WebSocket upgrade of chatgpt.com/backend-api/codex/
|
|
5
5
|
* responses (responses_websockets=2026-02-06 beta). Authenticates via PKCE
|
|
6
|
-
* OAuth
|
|
6
|
+
* OAuth using Mixdog-owned token storage. Streaming/framing lives in
|
|
7
7
|
* openai-oauth-ws.mjs; this file owns auth, model catalog, request-body
|
|
8
8
|
* shape, and HTTP/SSE fallback when WebSocket transport is unhealthy.
|
|
9
9
|
*/
|
|
@@ -11,7 +11,6 @@ import { createServer } from 'http';
|
|
|
11
11
|
import { randomBytes, createHash } from 'crypto';
|
|
12
12
|
import { readFileSync, existsSync, mkdirSync, statSync, unlinkSync } from 'fs';
|
|
13
13
|
import { join } from 'path';
|
|
14
|
-
import { homedir } from 'os';
|
|
15
14
|
import { getPluginData } from '../config.mjs';
|
|
16
15
|
import { enrichModels } from './model-catalog.mjs';
|
|
17
16
|
import { writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
|
|
@@ -22,13 +21,13 @@ import {
|
|
|
22
21
|
buildStableProviderPromptCacheKey,
|
|
23
22
|
resolveProviderPromptCacheLane,
|
|
24
23
|
resolveProviderCacheKey,
|
|
25
|
-
} from '../
|
|
24
|
+
} from '../agent-runtime/cache-strategy.mjs';
|
|
26
25
|
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
} from '../
|
|
26
|
+
appendAgentTrace,
|
|
27
|
+
traceAgentFetch,
|
|
28
|
+
traceAgentSse,
|
|
29
|
+
traceAgentUsage,
|
|
30
|
+
} from '../agent-trace.mjs';
|
|
32
31
|
import {
|
|
33
32
|
PROVIDER_GENERATE_TOTAL_TIMEOUT_MS,
|
|
34
33
|
PROVIDER_HTTP_RESPONSE_TIMEOUT_MS,
|
|
@@ -36,24 +35,28 @@ import {
|
|
|
36
35
|
} from '../stall-policy.mjs';
|
|
37
36
|
import { populateHttpStatusFromMessage } from './retry-classifier.mjs';
|
|
38
37
|
import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
|
|
38
|
+
import { makeInvalidToolArgsMarker } from './openai-compat-stream.mjs';
|
|
39
39
|
import {
|
|
40
|
-
contentHasImage,
|
|
41
40
|
normalizeContentForOpenAIResponses,
|
|
42
41
|
splitToolContentForOpenAIResponses,
|
|
43
42
|
} from './media-normalization.mjs';
|
|
43
|
+
import {
|
|
44
|
+
customToolCallFromResponseItem,
|
|
45
|
+
customToolInputFromArguments,
|
|
46
|
+
isCustomToolCallRecord,
|
|
47
|
+
isResponsesFreeformTool,
|
|
48
|
+
toResponsesCustomTool,
|
|
49
|
+
} from './custom-tool-wire.mjs';
|
|
44
50
|
// --- Constants ---
|
|
45
51
|
const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
46
52
|
const CODEX_OAUTH_ORIGINATOR = 'codex_cli_rs';
|
|
47
53
|
const TOKEN_URL = 'https://auth.openai.com/oauth/token';
|
|
48
54
|
const CODEX_RESPONSES_URL = 'https://chatgpt.com/backend-api/codex/responses';
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const REMOTE_COMPACT_RETAINED_TOKEN_BUDGET = 64_000;
|
|
52
|
-
// Version string baked into the models endpoint query — Codex rejects the
|
|
53
|
-
// request without it. Keep close to the latest published Codex CLI because
|
|
55
|
+
// Version string baked into the models endpoint query — the OAuth backend rejects the
|
|
56
|
+
// request without it. Keep close to the latest published @openai/codex CLI because
|
|
54
57
|
// older versions trigger a visibility-filtered catalog (e.g. only rollout
|
|
55
58
|
// models). Bump when the real CLI bumps.
|
|
56
|
-
//
|
|
59
|
+
// OpenAI OAuth backend gates new model exposures (e.g. gpt-5.5 only on >= 0.130.0)
|
|
57
60
|
// on the client_version header. Resolve dynamically from npm so newly-shipped
|
|
58
61
|
// models surface within a day instead of waiting on a hardcoded bump here.
|
|
59
62
|
// Cached 24h in-process; npm failure falls back to the floor below.
|
|
@@ -149,7 +152,7 @@ export function codexModelSupportsServiceTier(id, serviceTier) {
|
|
|
149
152
|
return tiers.some(t => t?.id === serviceTier);
|
|
150
153
|
}
|
|
151
154
|
|
|
152
|
-
//
|
|
155
|
+
// OAuth catalog returns dated ids (gpt-5.4-mini-2026-03-17). Strip the trailing
|
|
153
156
|
// -YYYY-MM-DD to get the version alias (gpt-5.4-mini). Unknown shapes pass
|
|
154
157
|
// through unchanged.
|
|
155
158
|
function _displayCodexModel(id) {
|
|
@@ -172,7 +175,7 @@ function _normalizeCodexModel(m) {
|
|
|
172
175
|
const additionalSpeedTiers = Array.isArray(m?.additional_speed_tiers)
|
|
173
176
|
? m.additional_speed_tiers.map(t => String(t || '').trim()).filter(Boolean)
|
|
174
177
|
: [];
|
|
175
|
-
//
|
|
178
|
+
// Catalog ids are version aliases without separate display dating.
|
|
176
179
|
return {
|
|
177
180
|
id,
|
|
178
181
|
name: m?.display_name || id,
|
|
@@ -206,8 +209,8 @@ function _codexFamily(id) {
|
|
|
206
209
|
return 'gpt';
|
|
207
210
|
}
|
|
208
211
|
|
|
209
|
-
// Compare two
|
|
210
|
-
// anthropic-oauth's _compareVersion
|
|
212
|
+
// Compare two model ids by the X.Y version embedded in `gpt-X.Y`. Mirrors
|
|
213
|
+
// anthropic-oauth's _compareVersion; these ids have no trailing date so
|
|
211
214
|
// the version lives in the dotted number, not a -YYYY-MM-DD suffix.
|
|
212
215
|
function _compareVersion(a, b) {
|
|
213
216
|
const na = (String(a).match(/gpt-(\d+)\.(\d+)/) || []).slice(1).map(Number);
|
|
@@ -225,7 +228,7 @@ function _isMainCodexFamily(family) {
|
|
|
225
228
|
}
|
|
226
229
|
|
|
227
230
|
// Mark the highest-version model per family as `latest: true`. VERSION-based
|
|
228
|
-
// (
|
|
231
|
+
// (ids carry no `created`), mirroring anthropic-oauth's per-family pass.
|
|
229
232
|
function _markLatestCodex(models) {
|
|
230
233
|
const byFamily = new Map();
|
|
231
234
|
for (const m of models) {
|
|
@@ -268,7 +271,7 @@ function getOwnTokenPath() {
|
|
|
268
271
|
}
|
|
269
272
|
|
|
270
273
|
// Public predicate used by config.buildDefaultConfig — provider is enabled
|
|
271
|
-
// when own tokens exist
|
|
274
|
+
// when own Mixdog tokens exist. Single truth:
|
|
272
275
|
// same loader the runtime uses (loadTokens), no parallel hard-coded path probe.
|
|
273
276
|
export function hasOpenAIOAuthCredentials() {
|
|
274
277
|
try {
|
|
@@ -281,7 +284,7 @@ export function describeOpenAIOAuthCredentials() {
|
|
|
281
284
|
try {
|
|
282
285
|
const tokens = loadTokens();
|
|
283
286
|
if (!tokens?.access_token) {
|
|
284
|
-
return { authenticated: false, status: 'Not Set', detail: '
|
|
287
|
+
return { authenticated: false, status: 'Not Set', detail: 'Mixdog token store' };
|
|
285
288
|
}
|
|
286
289
|
const hasRefresh = Boolean(tokens.refresh_token);
|
|
287
290
|
const expiresAt = _normalizeExpiresAt(tokens.expires_at ?? tokens.expiresAt);
|
|
@@ -310,7 +313,7 @@ function _normalizeExpiresAt(value) {
|
|
|
310
313
|
}
|
|
311
314
|
function _tokensMaxMtime() {
|
|
312
315
|
let max = 0;
|
|
313
|
-
const paths = [getOwnTokenPath()
|
|
316
|
+
const paths = [getOwnTokenPath()];
|
|
314
317
|
for (const p of paths) {
|
|
315
318
|
try {
|
|
316
319
|
const s = statSync(p);
|
|
@@ -319,10 +322,6 @@ function _tokensMaxMtime() {
|
|
|
319
322
|
}
|
|
320
323
|
return max;
|
|
321
324
|
}
|
|
322
|
-
|
|
323
|
-
function _codexCliAuthPath() {
|
|
324
|
-
return join(homedir(), '.codex', 'auth.json');
|
|
325
|
-
}
|
|
326
325
|
function _loadOwnCodexTokens() {
|
|
327
326
|
const ownPath = getOwnTokenPath();
|
|
328
327
|
if (!existsSync(ownPath)) return null;
|
|
@@ -334,7 +333,7 @@ function _loadOwnCodexTokens() {
|
|
|
334
333
|
...own,
|
|
335
334
|
expires_at: _normalizeExpiresAt(own.expires_at ?? own.expiresAt) || _expiryFromAccessToken(own.access_token),
|
|
336
335
|
account_id: own.account_id || extractAccountId(own.access_token),
|
|
337
|
-
source: '
|
|
336
|
+
source: 'Mixdog token store',
|
|
338
337
|
_mtimeMs: stat.mtimeMs,
|
|
339
338
|
};
|
|
340
339
|
}
|
|
@@ -342,38 +341,8 @@ function _loadOwnCodexTokens() {
|
|
|
342
341
|
catch { /* fall through */ }
|
|
343
342
|
return null;
|
|
344
343
|
}
|
|
345
|
-
function _loadCodexCliTokens() {
|
|
346
|
-
const codexPath = _codexCliAuthPath();
|
|
347
|
-
if (!existsSync(codexPath)) return null;
|
|
348
|
-
try {
|
|
349
|
-
const stat = statSync(codexPath);
|
|
350
|
-
const data = JSON.parse(readFileSync(codexPath, 'utf-8'));
|
|
351
|
-
const tokens = data.tokens || data;
|
|
352
|
-
if (tokens.access_token && tokens.refresh_token) {
|
|
353
|
-
const expiresAt = _normalizeExpiresAt(data.expires_at ?? tokens.expires_at ?? data.expiresAt ?? tokens.expiresAt) || _expiryFromAccessToken(tokens.access_token);
|
|
354
|
-
return {
|
|
355
|
-
access_token: tokens.access_token,
|
|
356
|
-
refresh_token: tokens.refresh_token,
|
|
357
|
-
expires_at: expiresAt,
|
|
358
|
-
account_id: tokens.account_id || extractAccountId(tokens.access_token),
|
|
359
|
-
source: 'codex-cli',
|
|
360
|
-
_mtimeMs: stat.mtimeMs,
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
catch { /* fall through */ }
|
|
365
|
-
return null;
|
|
366
|
-
}
|
|
367
|
-
// Own store is authoritative (accurate expires_at from refresh); the Codex CLI
|
|
368
|
-
// store seeds the initial bootstrap. But the refresh-token lineage is shared
|
|
369
|
-
// single-use with the Codex CLI, so when the CLI store is STRICTLY newer on
|
|
370
|
-
// disk (an independent `codex login`/CLI refresh) we must adopt it instead of
|
|
371
|
-
// replaying our consumed token. Freshest-wins, own preferred on a tie.
|
|
372
344
|
function loadTokens() {
|
|
373
|
-
|
|
374
|
-
const cli = _loadCodexCliTokens();
|
|
375
|
-
if (own && cli) return (cli._mtimeMs > own._mtimeMs) ? cli : own;
|
|
376
|
-
return own || cli;
|
|
345
|
+
return _loadOwnCodexTokens();
|
|
377
346
|
}
|
|
378
347
|
function saveTokens(tokens) {
|
|
379
348
|
const target = getOwnTokenPath();
|
|
@@ -387,55 +356,8 @@ export function forgetOpenAIOAuthCredentials() {
|
|
|
387
356
|
unlinkSync(ownPath);
|
|
388
357
|
removed = true;
|
|
389
358
|
}
|
|
390
|
-
const codexPath = _codexCliAuthPath();
|
|
391
|
-
if (existsSync(codexPath)) {
|
|
392
|
-
try {
|
|
393
|
-
const raw = JSON.parse(readFileSync(codexPath, 'utf-8'));
|
|
394
|
-
if (raw?.tokens && typeof raw.tokens === 'object') {
|
|
395
|
-
delete raw.tokens.access_token;
|
|
396
|
-
delete raw.tokens.refresh_token;
|
|
397
|
-
delete raw.tokens.id_token;
|
|
398
|
-
raw.last_refresh = new Date().toISOString();
|
|
399
|
-
writeJsonAtomicSync(codexPath, raw, { lock: true, fsyncDir: true });
|
|
400
|
-
removed = true;
|
|
401
|
-
} else if (raw?.access_token || raw?.refresh_token) {
|
|
402
|
-
delete raw.access_token;
|
|
403
|
-
delete raw.refresh_token;
|
|
404
|
-
delete raw.id_token;
|
|
405
|
-
raw.last_refresh = new Date().toISOString();
|
|
406
|
-
writeJsonAtomicSync(codexPath, raw, { lock: true, fsyncDir: true });
|
|
407
|
-
removed = true;
|
|
408
|
-
}
|
|
409
|
-
} catch (err) {
|
|
410
|
-
throw new Error(`OpenAI OAuth reset failed for ${codexPath}: ${err?.message || err}`);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
359
|
return { removed };
|
|
414
360
|
}
|
|
415
|
-
// Write rotated tokens back to the Codex CLI store (~/.codex/auth.json) so the
|
|
416
|
-
// Codex CLI picks up the rotation instead of replaying a consumed refresh_token
|
|
417
|
-
// from the shared single-use lineage. Mirrors anthropic-oauth's write-back.
|
|
418
|
-
// Best-effort; the own store stays authoritative. Host-owned file: preserve all
|
|
419
|
-
// other fields and don't re-permission it (no secret/mode).
|
|
420
|
-
function _writeBackCodexCliTokens(tokens) {
|
|
421
|
-
const path = _codexCliAuthPath();
|
|
422
|
-
if (!existsSync(path)) return;
|
|
423
|
-
try {
|
|
424
|
-
const raw = JSON.parse(readFileSync(path, 'utf-8'));
|
|
425
|
-
if (!raw || typeof raw !== 'object') return;
|
|
426
|
-
const slot = (raw.tokens && typeof raw.tokens === 'object') ? raw.tokens : raw;
|
|
427
|
-
slot.access_token = tokens.access_token;
|
|
428
|
-
slot.refresh_token = tokens.refresh_token;
|
|
429
|
-
raw.last_refresh = new Date().toISOString();
|
|
430
|
-
// Preserve the Codex CLI file's existing POSIX mode (writeJsonAtomicSync
|
|
431
|
-
// otherwise defaults to 0o600, re-permissioning a host-owned file).
|
|
432
|
-
let mode;
|
|
433
|
-
try { mode = statSync(path).mode & 0o777; } catch { /* keep helper default */ }
|
|
434
|
-
writeJsonAtomicSync(path, raw, { lock: true, fsyncDir: true, mode });
|
|
435
|
-
} catch (err) {
|
|
436
|
-
process.stderr.write(`[openai-oauth] Codex CLI store write-back failed: ${String(err?.message || err).slice(0, 200)}\n`);
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
361
|
function extractAccountId(token) {
|
|
440
362
|
try {
|
|
441
363
|
const parts = token.split('.');
|
|
@@ -449,11 +371,7 @@ function extractAccountId(token) {
|
|
|
449
371
|
}
|
|
450
372
|
}
|
|
451
373
|
// Derive token expiry from the access_token's JWT `exp` claim (epoch ms), as a
|
|
452
|
-
// fallback when the
|
|
453
|
-
// CLI's ~/.codex/auth.json records only last_refresh, so expires_at resolves to 0
|
|
454
|
-
// and ensureAuth reads that as "never expires", disabling proactive refresh; the
|
|
455
|
-
// token then only refreshes reactively after a request fails (and a WS handshake
|
|
456
|
-
// 401 can surface as an opaque transport error that the 401 path misses). Returns
|
|
374
|
+
// fallback when the Mixdog token store carries no explicit expires_at. Returns
|
|
457
375
|
// 0 for opaque (non-JWT) tokens. JWT `exp` is epoch SECONDS (RFC 7519).
|
|
458
376
|
function _expiryFromAccessToken(token) {
|
|
459
377
|
try {
|
|
@@ -487,7 +405,7 @@ async function refreshTokens(refreshToken) {
|
|
|
487
405
|
});
|
|
488
406
|
if (!res.ok) {
|
|
489
407
|
const text = await res.text().catch(() => '');
|
|
490
|
-
// Distinguish a terminally-dead refresh token (consumed by the
|
|
408
|
+
// Distinguish a terminally-dead refresh token (consumed by the official
|
|
491
409
|
// CLI's single-use lineage) from transient failures, so the caller can
|
|
492
410
|
// re-read disk and retry once with a newer token instead of
|
|
493
411
|
// collapsing every failure to a generic null.
|
|
@@ -506,11 +424,6 @@ async function refreshTokens(refreshToken) {
|
|
|
506
424
|
expires_at: expiresAt,
|
|
507
425
|
account_id: extractAccountId(json.access_token),
|
|
508
426
|
};
|
|
509
|
-
// CLI store first, own store last: the own store keeps the newest mtime
|
|
510
|
-
// (and its accurate refresh expires_at), so freshest-wins loadTokens
|
|
511
|
-
// treats our refresh as authoritative while the CLI still picks up the
|
|
512
|
-
// rotated token.
|
|
513
|
-
_writeBackCodexCliTokens(tokens);
|
|
514
427
|
saveTokens(tokens);
|
|
515
428
|
return tokens;
|
|
516
429
|
} catch (err) {
|
|
@@ -526,18 +439,6 @@ function _cloneJson(value) {
|
|
|
526
439
|
try { return JSON.parse(JSON.stringify(value)); } catch { return value; }
|
|
527
440
|
}
|
|
528
441
|
|
|
529
|
-
function _nativeCompactState(providerState, model) {
|
|
530
|
-
const state = providerState?.openaiCodex?.remoteCompact;
|
|
531
|
-
if (!state || state.version !== OPENAI_CODEX_REMOTE_COMPACT_VERSION) return null;
|
|
532
|
-
if (!Array.isArray(state.nativePrefix) || state.nativePrefix.length === 0) return null;
|
|
533
|
-
if (state.model && model && state.model !== model) return null;
|
|
534
|
-
return state;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
function _isRemoteCompactFallbackMessage(m) {
|
|
538
|
-
return m?._mixdogRemoteCompactFallback === OPENAI_CODEX_REMOTE_COMPACT_FALLBACK;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
442
|
function _contentTextParts(content, type = 'input_text') {
|
|
542
443
|
if (typeof content === 'string') return content ? [{ type, text: content }] : [];
|
|
543
444
|
if (!Array.isArray(content)) {
|
|
@@ -556,127 +457,41 @@ function _contentTextParts(content, type = 'input_text') {
|
|
|
556
457
|
return out;
|
|
557
458
|
}
|
|
558
459
|
|
|
559
|
-
function _messageToNativeRetainedItem(m) {
|
|
560
|
-
const role = m?.role;
|
|
561
|
-
if (role !== 'user') return null;
|
|
562
|
-
if (_isRemoteCompactFallbackMessage(m)) return null;
|
|
563
|
-
const content = _contentTextParts(m.content, 'input_text');
|
|
564
|
-
if (!content.length) return null;
|
|
565
|
-
return { type: 'message', role, content };
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
function _nativeMessageTextTokenCount(item) {
|
|
569
|
-
if (!item || item.type !== 'message') return 0;
|
|
570
|
-
const content = Array.isArray(item.content) ? item.content : [];
|
|
571
|
-
let chars = 0;
|
|
572
|
-
for (const part of content) {
|
|
573
|
-
if (typeof part?.text === 'string') chars += part.text.length;
|
|
574
|
-
}
|
|
575
|
-
return Math.max(1, Math.ceil(chars / 4));
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function _truncateTextForTokens(text, maxTokens) {
|
|
579
|
-
const value = String(text || '');
|
|
580
|
-
const maxChars = Math.max(0, Math.floor(maxTokens * 4));
|
|
581
|
-
if (value.length <= maxChars) return value;
|
|
582
|
-
if (maxChars <= 32) return value.slice(0, maxChars);
|
|
583
|
-
const marker = `...${Math.max(1, Math.ceil((value.length - maxChars) / 4))} tokens truncated...`;
|
|
584
|
-
const room = Math.max(0, maxChars - marker.length);
|
|
585
|
-
const head = Math.ceil(room / 2);
|
|
586
|
-
const tail = Math.floor(room / 2);
|
|
587
|
-
return `${value.slice(0, head)}${marker}${value.slice(value.length - tail)}`;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
function _truncateNativeMessageToTokenBudget(item, maxTokens) {
|
|
591
|
-
if (!item || item.type !== 'message') return item;
|
|
592
|
-
const clone = _cloneJson(item);
|
|
593
|
-
let remaining = Math.max(0, Math.floor(maxTokens));
|
|
594
|
-
const content = [];
|
|
595
|
-
for (const part of Array.isArray(clone.content) ? clone.content : []) {
|
|
596
|
-
if (typeof part?.text !== 'string') {
|
|
597
|
-
content.push(part);
|
|
598
|
-
continue;
|
|
599
|
-
}
|
|
600
|
-
if (remaining <= 0) continue;
|
|
601
|
-
const tokenCount = Math.max(1, Math.ceil(part.text.length / 4));
|
|
602
|
-
if (tokenCount <= remaining) {
|
|
603
|
-
content.push(part);
|
|
604
|
-
remaining -= tokenCount;
|
|
605
|
-
} else {
|
|
606
|
-
const truncated = _truncateTextForTokens(part.text, remaining);
|
|
607
|
-
if (truncated) content.push({ ...part, text: truncated });
|
|
608
|
-
remaining = 0;
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
if (!content.length) return null;
|
|
612
|
-
clone.content = content;
|
|
613
|
-
return clone;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
function _truncateRetainedNativeMessages(items, maxTokens = REMOTE_COMPACT_RETAINED_TOKEN_BUDGET) {
|
|
617
|
-
let remaining = Math.max(0, Math.floor(maxTokens));
|
|
618
|
-
const reversed = [];
|
|
619
|
-
for (let i = items.length - 1; i >= 0; i -= 1) {
|
|
620
|
-
if (remaining <= 0) break;
|
|
621
|
-
const item = items[i];
|
|
622
|
-
const tokenCount = _nativeMessageTextTokenCount(item);
|
|
623
|
-
if (tokenCount <= remaining) {
|
|
624
|
-
reversed.push(item);
|
|
625
|
-
remaining -= tokenCount;
|
|
626
|
-
continue;
|
|
627
|
-
}
|
|
628
|
-
const truncated = _truncateNativeMessageToTokenBudget(item, remaining);
|
|
629
|
-
if (truncated) reversed.push(truncated);
|
|
630
|
-
remaining = 0;
|
|
631
|
-
}
|
|
632
|
-
return reversed.reverse();
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
function _buildOpenAICodexNativeCompactPrefix(messages, compactionItem) {
|
|
636
|
-
const retained = [];
|
|
637
|
-
for (const m of messages || []) {
|
|
638
|
-
const item = _messageToNativeRetainedItem(m);
|
|
639
|
-
if (item) retained.push(item);
|
|
640
|
-
}
|
|
641
|
-
const compact = _cloneJson(compactionItem);
|
|
642
|
-
return [..._truncateRetainedNativeMessages(retained), compact];
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
function _withOpenAICodexRemoteCompactState(providerState, { model, nativePrefix, responseId } = {}) {
|
|
646
|
-
return {
|
|
647
|
-
...(providerState || {}),
|
|
648
|
-
openaiCodex: {
|
|
649
|
-
...(providerState?.openaiCodex || {}),
|
|
650
|
-
remoteCompact: {
|
|
651
|
-
version: OPENAI_CODEX_REMOTE_COMPACT_VERSION,
|
|
652
|
-
model: model || null,
|
|
653
|
-
nativePrefix,
|
|
654
|
-
responseId: responseId || null,
|
|
655
|
-
installedAt: Date.now(),
|
|
656
|
-
},
|
|
657
|
-
},
|
|
658
|
-
};
|
|
659
|
-
}
|
|
660
|
-
|
|
661
460
|
/**
|
|
662
461
|
* Convert a message slice to Responses API input items.
|
|
663
462
|
*/
|
|
664
463
|
function convertMessagesToResponsesInput(messages, opts = {}) {
|
|
665
464
|
const out = [];
|
|
666
|
-
const nativeCompact = _nativeCompactState(opts.providerState, opts.model);
|
|
667
|
-
if (nativeCompact) {
|
|
668
|
-
for (const item of nativeCompact.nativePrefix) out.push(_cloneJson(item));
|
|
669
|
-
}
|
|
670
465
|
const pendingToolMedia = [];
|
|
466
|
+
const customToolCallNameById = new Map();
|
|
671
467
|
const flushToolMedia = () => {
|
|
672
468
|
if (!pendingToolMedia.length) return;
|
|
673
469
|
out.push({ role: 'user', content: pendingToolMedia.splice(0) });
|
|
674
470
|
};
|
|
675
471
|
for (const m of messages) {
|
|
676
472
|
if (!m || m.role === 'system') continue;
|
|
677
|
-
if (nativeCompact && _isRemoteCompactFallbackMessage(m)) continue;
|
|
678
473
|
if (m.role === 'tool') {
|
|
474
|
+
if (Array.isArray(m.nativeToolSearch?.openaiTools)) {
|
|
475
|
+
out.push({
|
|
476
|
+
type: 'tool_search_output',
|
|
477
|
+
call_id: m.toolCallId || '',
|
|
478
|
+
status: 'completed',
|
|
479
|
+
execution: 'client',
|
|
480
|
+
tools: m.nativeToolSearch.openaiTools,
|
|
481
|
+
});
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
679
484
|
const { output, mediaContent } = splitToolContentForOpenAIResponses(m.content);
|
|
485
|
+
if (customToolCallNameById.has(m.toolCallId || '')) {
|
|
486
|
+
out.push({
|
|
487
|
+
type: 'custom_tool_call_output',
|
|
488
|
+
call_id: m.toolCallId || '',
|
|
489
|
+
name: customToolCallNameById.get(m.toolCallId || '') || undefined,
|
|
490
|
+
output,
|
|
491
|
+
});
|
|
492
|
+
if (mediaContent) pendingToolMedia.push(...mediaContent);
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
680
495
|
out.push({
|
|
681
496
|
type: 'function_call_output',
|
|
682
497
|
call_id: m.toolCallId || '',
|
|
@@ -687,7 +502,7 @@ function convertMessagesToResponsesInput(messages, opts = {}) {
|
|
|
687
502
|
}
|
|
688
503
|
flushToolMedia();
|
|
689
504
|
if (m.role === 'assistant' && Array.isArray(m.toolCalls) && m.toolCalls.length) {
|
|
690
|
-
// Reasoning replay deliberately omitted:
|
|
505
|
+
// Reasoning replay deliberately omitted: openai-oauth rejects an
|
|
691
506
|
// `rs_*` reasoning item with the same id across the same
|
|
692
507
|
// handshake session_id (in-memory conversation state lives
|
|
693
508
|
// for the WS_IDLE_MS window even after a socket close).
|
|
@@ -695,12 +510,29 @@ function convertMessagesToResponsesInput(messages, opts = {}) {
|
|
|
695
510
|
// reasoning in `input` triggers "Duplicate item".
|
|
696
511
|
if (m.content) out.push({ role: 'assistant', content: normalizeContentForOpenAIResponses(m.content, { role: 'assistant' }) });
|
|
697
512
|
for (const tc of m.toolCalls) {
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
513
|
+
if (tc.nativeType === 'tool_search_call' || tc.name === 'tool_search') {
|
|
514
|
+
out.push({
|
|
515
|
+
type: 'tool_search_call',
|
|
516
|
+
call_id: tc.id,
|
|
517
|
+
execution: 'client',
|
|
518
|
+
arguments: tc.arguments || {},
|
|
519
|
+
});
|
|
520
|
+
} else if (isCustomToolCallRecord(tc)) {
|
|
521
|
+
if (tc.id) customToolCallNameById.set(tc.id, tc.name || '');
|
|
522
|
+
out.push({
|
|
523
|
+
type: 'custom_tool_call',
|
|
524
|
+
call_id: tc.id,
|
|
525
|
+
name: tc.name,
|
|
526
|
+
input: customToolInputFromArguments(tc.name, tc.arguments),
|
|
527
|
+
});
|
|
528
|
+
} else {
|
|
529
|
+
out.push({
|
|
530
|
+
type: 'function_call',
|
|
531
|
+
call_id: tc.id,
|
|
532
|
+
name: tc.name,
|
|
533
|
+
arguments: JSON.stringify(tc.arguments),
|
|
534
|
+
});
|
|
535
|
+
}
|
|
704
536
|
}
|
|
705
537
|
continue;
|
|
706
538
|
}
|
|
@@ -713,8 +545,22 @@ function convertMessagesToResponsesInput(messages, opts = {}) {
|
|
|
713
545
|
return out;
|
|
714
546
|
}
|
|
715
547
|
|
|
716
|
-
function
|
|
717
|
-
|
|
548
|
+
function toOpenAIResponsesTool(t) {
|
|
549
|
+
if (t?.name === 'tool_search') {
|
|
550
|
+
return {
|
|
551
|
+
type: 'tool_search',
|
|
552
|
+
execution: 'client',
|
|
553
|
+
description: t.description,
|
|
554
|
+
parameters: t.inputSchema,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
if (isResponsesFreeformTool(t)) return toResponsesCustomTool(t);
|
|
558
|
+
return {
|
|
559
|
+
type: 'function',
|
|
560
|
+
name: t.name,
|
|
561
|
+
description: t.description,
|
|
562
|
+
parameters: t.inputSchema,
|
|
563
|
+
};
|
|
718
564
|
}
|
|
719
565
|
|
|
720
566
|
export function buildRequestBody(messages, model, tools, sendOpts) {
|
|
@@ -726,11 +572,16 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
726
572
|
providerState: opts.providerState,
|
|
727
573
|
model,
|
|
728
574
|
});
|
|
729
|
-
// Match the body shape pi-mono and the official
|
|
575
|
+
// Match the body shape pi-mono and the official OpenAI CLI ship so the
|
|
730
576
|
// server-side auto-cache routes correctly. text.verbosity / include /
|
|
731
577
|
// tool_choice / parallel_tool_calls are all inert without side effects
|
|
732
|
-
// for most callers but their presence affects how
|
|
578
|
+
// for most callers but their presence affects how the OAuth backend classifies the
|
|
733
579
|
// request (and therefore whether the prompt cache is consulted).
|
|
580
|
+
const include = ['reasoning.encrypted_content'];
|
|
581
|
+
for (const item of Array.isArray(opts.nativeInclude) ? opts.nativeInclude : []) {
|
|
582
|
+
const value = String(item || '').trim();
|
|
583
|
+
if (value && !include.includes(value)) include.push(value);
|
|
584
|
+
}
|
|
734
585
|
const body = {
|
|
735
586
|
model,
|
|
736
587
|
instructions,
|
|
@@ -739,7 +590,7 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
739
590
|
stream: true,
|
|
740
591
|
reasoning: { effort: opts.effort || 'medium' },
|
|
741
592
|
text: { verbosity: 'medium' },
|
|
742
|
-
include
|
|
593
|
+
include,
|
|
743
594
|
tool_choice: opts.toolChoice || 'auto',
|
|
744
595
|
parallel_tool_calls: true,
|
|
745
596
|
};
|
|
@@ -750,22 +601,23 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
750
601
|
body.max_output_tokens = Math.floor(maxOutputTokens);
|
|
751
602
|
}
|
|
752
603
|
if (opts.fast === true) {
|
|
753
|
-
// 'priority' is the only fast-class value the
|
|
604
|
+
// 'priority' is the only fast-class value the OpenAI OAuth backend
|
|
754
605
|
// accepts on the wire: 'fast' is hard-rejected ("Unsupported
|
|
755
|
-
// service_tier: fast", probed 2026-06-11). Match official
|
|
606
|
+
// service_tier: fast", probed 2026-06-11). Match official CLI behavior:
|
|
756
607
|
// only send the request value when the model catalog advertises it.
|
|
757
608
|
if (codexModelSupportsServiceTier(model, 'priority')) {
|
|
758
609
|
body.service_tier = 'priority';
|
|
759
610
|
}
|
|
760
611
|
}
|
|
761
|
-
// Add tools
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
612
|
+
// Add tools. `nativeTools` are server-hosted Responses tools (for
|
|
613
|
+
// example web_search) and must be passed through without wrapping them as
|
|
614
|
+
// function tools.
|
|
615
|
+
const functionTools = tools?.length ? tools.map(toOpenAIResponsesTool) : [];
|
|
616
|
+
const nativeTools = Array.isArray(opts.nativeTools)
|
|
617
|
+
? opts.nativeTools.filter(t => t && typeof t === 'object')
|
|
618
|
+
: [];
|
|
619
|
+
if (functionTools.length || nativeTools.length) {
|
|
620
|
+
body.tools = [...nativeTools, ...functionTools];
|
|
769
621
|
}
|
|
770
622
|
const promptCacheProvider = opts.promptCacheProvider || 'openai-oauth';
|
|
771
623
|
const promptCacheLane = opts.promptCacheLane || resolveProviderPromptCacheLane(promptCacheProvider, opts);
|
|
@@ -782,21 +634,9 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
782
634
|
cacheLaneShards: promptCacheLane.shards,
|
|
783
635
|
});
|
|
784
636
|
// NOTE: prompt_cache_retention is a public OpenAI Responses API parameter,
|
|
785
|
-
// but the
|
|
637
|
+
// but the openai-oauth endpoint still rejects it ("Unsupported parameter:
|
|
786
638
|
// prompt_cache_retention", re-probed 2026-06-22). Leave retention on the
|
|
787
|
-
//
|
|
788
|
-
return body;
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
function buildRemoteCompactionRequestBody(messages, model, tools, sendOpts) {
|
|
792
|
-
const body = buildRequestBody(messages, model, tools, {
|
|
793
|
-
...(sendOpts || {}),
|
|
794
|
-
expectCompaction: true,
|
|
795
|
-
});
|
|
796
|
-
body.input = [
|
|
797
|
-
...(Array.isArray(body.input) ? body.input : []),
|
|
798
|
-
{ type: 'compaction_trigger' },
|
|
799
|
-
];
|
|
639
|
+
// openai-oauth server default; public OpenAI direct injects 24h separately.
|
|
800
640
|
return body;
|
|
801
641
|
}
|
|
802
642
|
|
|
@@ -806,12 +646,29 @@ function _envFlag(name, fallback = true) {
|
|
|
806
646
|
return !['0', 'false', 'off', 'no'].includes(String(raw).toLowerCase());
|
|
807
647
|
}
|
|
808
648
|
|
|
649
|
+
function _envPositiveInt(name, fallback) {
|
|
650
|
+
const raw = process.env[name];
|
|
651
|
+
if (raw == null || raw === '') return fallback;
|
|
652
|
+
const n = Number(raw);
|
|
653
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Completed function_call.arguments parse for the OpenAI Responses stream.
|
|
657
|
+
// Native convergence (openai-oauth / anthropic-oauth / opencode): a function_call item
|
|
658
|
+
// arrives only on a completion/done signal, so a non-empty-but-malformed
|
|
659
|
+
// arguments string is deterministic bad JSON — NOT mid-stream truncation.
|
|
660
|
+
// Empty/whitespace input legitimately means "no arguments" → {}. A non-empty
|
|
661
|
+
// string that fails JSON.parse is surfaced as an invalid-args MARKER (instead
|
|
662
|
+
// of being silently swallowed to {}) so the dispatch loop turns it into an
|
|
663
|
+
// is_error tool_result and the model self-corrects in the same turn.
|
|
809
664
|
function _parseJsonObject(value) {
|
|
665
|
+
const text = typeof value === 'string' ? value : (value == null ? '' : String(value));
|
|
666
|
+
if (text.trim() === '') return {};
|
|
810
667
|
try {
|
|
811
|
-
const parsed = JSON.parse(
|
|
668
|
+
const parsed = JSON.parse(text);
|
|
812
669
|
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
813
|
-
} catch {
|
|
814
|
-
return
|
|
670
|
+
} catch (err) {
|
|
671
|
+
return makeInvalidToolArgsMarker(text, err instanceof Error ? err.message : String(err));
|
|
815
672
|
}
|
|
816
673
|
}
|
|
817
674
|
|
|
@@ -892,6 +749,7 @@ function _shouldUseOpenAIHttpFallback(err, externalSignal) {
|
|
|
892
749
|
// chunk to the client, the HTTP fallback would re-run the request and
|
|
893
750
|
// concatenate a second attempt onto rendered output. Never fall back.
|
|
894
751
|
if (err?.liveTextEmitted === true) return false;
|
|
752
|
+
if (err?.emittedToolCall === true || err?.unsafeToRetry === true) return false;
|
|
895
753
|
const status = Number(err?.httpStatus || err?.status || 0);
|
|
896
754
|
if (status === 401 || status === 403 || status === 404 || status === 429) return false;
|
|
897
755
|
if (status >= 500 && status < 600) return true;
|
|
@@ -900,7 +758,13 @@ function _shouldUseOpenAIHttpFallback(err, externalSignal) {
|
|
|
900
758
|
return true;
|
|
901
759
|
}
|
|
902
760
|
const classifier = String(err?.retryClassifier || err?.midstreamClassifier || '');
|
|
903
|
-
if ([
|
|
761
|
+
if ([
|
|
762
|
+
'timeout', 'reset', 'dns', 'refused', 'network', 'acquire_timeout', 'http_5xx',
|
|
763
|
+
'first_byte_timeout', 'first_meaningful_timeout',
|
|
764
|
+
'ws_1006', 'ws_1011', 'ws_1012', 'ws_1000', 'ws_4000', 'agent_stall', 'stream_stalled',
|
|
765
|
+
'response_failed_disconnected', 'response_failed_network', 'response_failed_auth_expired',
|
|
766
|
+
'ws_send_failed',
|
|
767
|
+
].includes(classifier)) {
|
|
904
768
|
return true;
|
|
905
769
|
}
|
|
906
770
|
if (/^http_5\d\d$/.test(classifier)) return true;
|
|
@@ -960,7 +824,7 @@ export async function sendViaHttpSse({
|
|
|
960
824
|
headerTimeout.cleanup();
|
|
961
825
|
}
|
|
962
826
|
|
|
963
|
-
|
|
827
|
+
traceAgentFetch({
|
|
964
828
|
sessionId: poolKey,
|
|
965
829
|
headersMs: Date.now() - fetchStartedAt,
|
|
966
830
|
httpStatus: response.status,
|
|
@@ -1019,7 +883,6 @@ export async function sendViaHttpSse({
|
|
|
1019
883
|
const toolCalls = [];
|
|
1020
884
|
const pendingCalls = new Map();
|
|
1021
885
|
const reasoningItems = [];
|
|
1022
|
-
const compactionItems = [];
|
|
1023
886
|
const citations = [];
|
|
1024
887
|
const citationKeys = new Set();
|
|
1025
888
|
const webSearchCalls = [];
|
|
@@ -1067,10 +930,34 @@ export async function sendViaHttpSse({
|
|
|
1067
930
|
});
|
|
1068
931
|
}
|
|
1069
932
|
};
|
|
1070
|
-
const
|
|
1071
|
-
if (!item ||
|
|
1072
|
-
|
|
1073
|
-
|
|
933
|
+
const pushToolSearchCall = (item) => {
|
|
934
|
+
if (!item || item.type !== 'tool_search_call') return;
|
|
935
|
+
const callId = item.call_id || item.id || '';
|
|
936
|
+
if (!callId || toolCalls.some(t => t.id === callId)) return;
|
|
937
|
+
let args = {};
|
|
938
|
+
if (item.arguments && typeof item.arguments === 'object') {
|
|
939
|
+
args = item.arguments;
|
|
940
|
+
} else if (typeof item.arguments === 'string' && item.arguments.trim()) {
|
|
941
|
+
// Non-empty but malformed tool_search arguments are deterministic
|
|
942
|
+
// bad JSON (the item is only emitted on completion). Surface an
|
|
943
|
+
// invalid-args marker instead of swallowing to {} so the model can
|
|
944
|
+
// self-correct in the same turn.
|
|
945
|
+
args = _parseJsonObject(item.arguments);
|
|
946
|
+
}
|
|
947
|
+
const call = {
|
|
948
|
+
id: callId,
|
|
949
|
+
name: 'tool_search',
|
|
950
|
+
arguments: args,
|
|
951
|
+
nativeType: 'tool_search_call',
|
|
952
|
+
};
|
|
953
|
+
toolCalls.push(call);
|
|
954
|
+
emitToolCall(call);
|
|
955
|
+
};
|
|
956
|
+
const pushCustomToolCall = (item) => {
|
|
957
|
+
const call = customToolCallFromResponseItem(item);
|
|
958
|
+
if (!call || toolCalls.some(t => t.id === call.id)) return;
|
|
959
|
+
toolCalls.push(call);
|
|
960
|
+
emitToolCall(call);
|
|
1074
961
|
};
|
|
1075
962
|
const meaningful = () => {
|
|
1076
963
|
if (ttftMs == null) ttftMs = Date.now() - sseStartedAt;
|
|
@@ -1123,11 +1010,13 @@ export async function sendViaHttpSse({
|
|
|
1123
1010
|
meaningful();
|
|
1124
1011
|
break;
|
|
1125
1012
|
}
|
|
1013
|
+
case 'response.custom_tool_call_input.delta':
|
|
1014
|
+
meaningful();
|
|
1015
|
+
break;
|
|
1126
1016
|
case 'response.output_item.done': {
|
|
1127
1017
|
const item = event.item || {};
|
|
1128
1018
|
pushReasoningItem(item);
|
|
1129
1019
|
pushWebSearchCall(item);
|
|
1130
|
-
pushCompactionItem(item);
|
|
1131
1020
|
if (item.type === 'function_call') {
|
|
1132
1021
|
const tc = toolCalls.find(t => t._pendingItemId === (item.id || ''));
|
|
1133
1022
|
if (tc) {
|
|
@@ -1138,6 +1027,11 @@ export async function sendViaHttpSse({
|
|
|
1138
1027
|
emitToolCall(tc);
|
|
1139
1028
|
}
|
|
1140
1029
|
}
|
|
1030
|
+
} else if (item.type === 'tool_search_call') {
|
|
1031
|
+
pushToolSearchCall(item);
|
|
1032
|
+
} else if (item.type === 'custom_tool_call') {
|
|
1033
|
+
pushCustomToolCall(item);
|
|
1034
|
+
meaningful();
|
|
1141
1035
|
}
|
|
1142
1036
|
break;
|
|
1143
1037
|
}
|
|
@@ -1165,8 +1059,11 @@ export async function sendViaHttpSse({
|
|
|
1165
1059
|
pushReasoningItem(item);
|
|
1166
1060
|
} else if (item.type === 'web_search_call') {
|
|
1167
1061
|
pushWebSearchCall(item);
|
|
1168
|
-
} else if (
|
|
1169
|
-
|
|
1062
|
+
} else if (item.type === 'tool_search_call') {
|
|
1063
|
+
pushToolSearchCall(item);
|
|
1064
|
+
} else if (item.type === 'custom_tool_call') {
|
|
1065
|
+
pushCustomToolCall(item);
|
|
1066
|
+
meaningful();
|
|
1170
1067
|
} else if (item.type === 'function_call') {
|
|
1171
1068
|
// Match the still-pending placeholder by item id, or
|
|
1172
1069
|
// an already-recorded call by its canonical call_id —
|
|
@@ -1282,15 +1179,12 @@ export async function sendViaHttpSse({
|
|
|
1282
1179
|
if (unresolved) {
|
|
1283
1180
|
throw new Error(`OpenAI OAuth HTTP fallback function_call salvage failed: missing call_id/name for item_id=${unresolved._pendingItemId || '?'}`);
|
|
1284
1181
|
}
|
|
1285
|
-
if (opts?.expectCompaction === true && compactionItems.length !== 1) {
|
|
1286
|
-
throw new Error(`OpenAI OAuth HTTP fallback remote compaction expected exactly one compaction output item, got ${compactionItems.length}`);
|
|
1287
|
-
}
|
|
1288
1182
|
if (!completed && !content && !toolCalls.length) {
|
|
1289
1183
|
throw new Error('OpenAI OAuth HTTP fallback ended before response.completed');
|
|
1290
1184
|
}
|
|
1291
1185
|
|
|
1292
1186
|
const liveModel = model || useModel;
|
|
1293
|
-
|
|
1187
|
+
traceAgentSse({
|
|
1294
1188
|
sessionId: poolKey,
|
|
1295
1189
|
sseParseMs: Date.now() - sseStartedAt,
|
|
1296
1190
|
ttftMs,
|
|
@@ -1299,7 +1193,7 @@ export async function sendViaHttpSse({
|
|
|
1299
1193
|
transport: 'sse',
|
|
1300
1194
|
});
|
|
1301
1195
|
if (usage) {
|
|
1302
|
-
|
|
1196
|
+
traceAgentUsage({
|
|
1303
1197
|
sessionId: poolKey,
|
|
1304
1198
|
iteration,
|
|
1305
1199
|
inputTokens: usage.inputTokens || 0,
|
|
@@ -1318,8 +1212,6 @@ export async function sendViaHttpSse({
|
|
|
1318
1212
|
content,
|
|
1319
1213
|
model: liveModel,
|
|
1320
1214
|
reasoningItems: reasoningItems.length ? reasoningItems : undefined,
|
|
1321
|
-
compactionItem: compactionItems.length === 1 ? compactionItems[0] : undefined,
|
|
1322
|
-
compactionItems: compactionItems.length ? compactionItems : undefined,
|
|
1323
1215
|
toolCalls: toolCalls.length ? toolCalls.map(({ _pendingItemId, ...t }) => t) : undefined,
|
|
1324
1216
|
citations: citations.length ? citations : undefined,
|
|
1325
1217
|
webSearchCalls: webSearchCalls.length ? webSearchCalls : undefined,
|
|
@@ -1339,137 +1231,29 @@ export class OpenAIOAuthProvider {
|
|
|
1339
1231
|
tokens = null;
|
|
1340
1232
|
_refreshFallbackUntil = 0;
|
|
1341
1233
|
_forceHttpFallback = false;
|
|
1234
|
+
_forceHttpFallbackUntil = 0;
|
|
1342
1235
|
config;
|
|
1343
1236
|
constructor(config) {
|
|
1344
1237
|
this.config = config || {};
|
|
1345
1238
|
this.tokens = loadTokens();
|
|
1346
|
-
// Warm a kept-alive socket to the
|
|
1239
|
+
// Warm a kept-alive socket to the OAuth responses API so the first
|
|
1347
1240
|
// request skips the cold TLS handshake. Best-effort; never throws.
|
|
1348
1241
|
preconnect('https://chatgpt.com');
|
|
1349
1242
|
}
|
|
1350
1243
|
getCachedModelInfo(model) {
|
|
1351
1244
|
return _findCachedCodexModel(model);
|
|
1352
1245
|
}
|
|
1353
|
-
async remoteCompactMessages(messages, model, tools, sendOpts = {}) {
|
|
1354
|
-
const opts = {
|
|
1355
|
-
...sendOpts,
|
|
1356
|
-
expectCompaction: true,
|
|
1357
|
-
nativeCompact: true,
|
|
1358
|
-
};
|
|
1359
|
-
const useModel = model || await ensureLatestCodexModel(this);
|
|
1360
|
-
const promptCacheLane = resolveProviderPromptCacheLane('openai-oauth', opts, this.config);
|
|
1361
|
-
const bodyOpts = {
|
|
1362
|
-
...opts,
|
|
1363
|
-
promptCacheLane,
|
|
1364
|
-
};
|
|
1365
|
-
const body = buildRemoteCompactionRequestBody(messages, useModel, tools, bodyOpts);
|
|
1366
|
-
let auth = await this.ensureAuth();
|
|
1367
|
-
const poolKey = opts.sessionId || null;
|
|
1368
|
-
const cacheKey = body.prompt_cache_key || resolveProviderCacheKey(opts, 'openai-oauth');
|
|
1369
|
-
const iteration = Number.isFinite(Number(opts.iteration)) ? Number(opts.iteration) : null;
|
|
1370
|
-
const onStageChange = typeof opts.onStageChange === 'function' ? opts.onStageChange : null;
|
|
1371
|
-
const sendWs = typeof opts._sendViaWebSocketFn === 'function' ? opts._sendViaWebSocketFn : sendViaWebSocket;
|
|
1372
|
-
const sendHttp = typeof opts._sendViaHttpSseFn === 'function' ? opts._sendViaHttpSseFn : sendViaHttpSse;
|
|
1373
|
-
const dispatchHttp = async (reason, originalErr = null) => {
|
|
1374
|
-
appendBridgeTrace({
|
|
1375
|
-
sessionId: poolKey,
|
|
1376
|
-
iteration,
|
|
1377
|
-
kind: 'transport_fallback',
|
|
1378
|
-
provider: 'openai-oauth',
|
|
1379
|
-
model: useModel,
|
|
1380
|
-
transport: 'http',
|
|
1381
|
-
payload: {
|
|
1382
|
-
from: 'websocket',
|
|
1383
|
-
to: 'http',
|
|
1384
|
-
reason,
|
|
1385
|
-
remote_compact: true,
|
|
1386
|
-
error_code: originalErr?.code || null,
|
|
1387
|
-
error_http_status: Number(originalErr?.httpStatus || 0) || null,
|
|
1388
|
-
},
|
|
1389
|
-
});
|
|
1390
|
-
return sendHttp({
|
|
1391
|
-
auth,
|
|
1392
|
-
body,
|
|
1393
|
-
opts,
|
|
1394
|
-
onStreamDelta: null,
|
|
1395
|
-
onToolCall: null,
|
|
1396
|
-
onStageChange,
|
|
1397
|
-
externalSignal: opts.signal || null,
|
|
1398
|
-
poolKey,
|
|
1399
|
-
cacheKey,
|
|
1400
|
-
iteration,
|
|
1401
|
-
useModel,
|
|
1402
|
-
fetchFn: opts._fetchFn,
|
|
1403
|
-
});
|
|
1404
|
-
};
|
|
1405
|
-
const dispatchWs = (forceFresh = false) => sendWs({
|
|
1406
|
-
auth,
|
|
1407
|
-
body,
|
|
1408
|
-
sendOpts: opts,
|
|
1409
|
-
onStreamDelta: null,
|
|
1410
|
-
onToolCall: null,
|
|
1411
|
-
onStageChange,
|
|
1412
|
-
externalSignal: opts.signal || null,
|
|
1413
|
-
poolKey,
|
|
1414
|
-
cacheKey,
|
|
1415
|
-
iteration,
|
|
1416
|
-
useModel,
|
|
1417
|
-
displayModel: _displayCodexModel,
|
|
1418
|
-
forceFresh,
|
|
1419
|
-
includeResponseId: true,
|
|
1420
|
-
});
|
|
1421
|
-
|
|
1422
|
-
let result;
|
|
1423
|
-
if (opts.forceHttpFallback === true
|
|
1424
|
-
|| this._forceHttpFallback
|
|
1425
|
-
|| _envFlag('MIXDOG_OPENAI_OAUTH_FORCE_HTTP_FALLBACK', false)) {
|
|
1426
|
-
result = await dispatchHttp('forced');
|
|
1427
|
-
} else {
|
|
1428
|
-
try {
|
|
1429
|
-
result = await dispatchWs(false);
|
|
1430
|
-
} catch (err) {
|
|
1431
|
-
const status = err?.httpStatus;
|
|
1432
|
-
if (status === 401 || status === 403) {
|
|
1433
|
-
this._refreshFallbackUntil = 0;
|
|
1434
|
-
auth = await this.ensureAuth({ forceRefresh: true, reason: String(status) });
|
|
1435
|
-
result = await dispatchWs(true);
|
|
1436
|
-
} else if (_shouldUseOpenAIHttpFallback(err, opts.signal || null)) {
|
|
1437
|
-
result = await dispatchHttp(err?.retryClassifier || err?.midstreamClassifier || err?.code || err?.message || 'ws_failed', err);
|
|
1438
|
-
} else {
|
|
1439
|
-
throw err;
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
if (!result?.compactionItem) {
|
|
1445
|
-
throw new Error('OpenAI OAuth remote compact completed without compaction item');
|
|
1446
|
-
}
|
|
1447
|
-
const liveModel = result.model || useModel;
|
|
1448
|
-
if (liveModel && !_codexCatalogHas(liveModel)) void this._refreshModelCache();
|
|
1449
|
-
const nativePrefix = _buildOpenAICodexNativeCompactPrefix(messages, result.compactionItem);
|
|
1450
|
-
return {
|
|
1451
|
-
model: liveModel,
|
|
1452
|
-
usage: result.usage,
|
|
1453
|
-
responseId: result.responseId || null,
|
|
1454
|
-
providerState: _withOpenAICodexRemoteCompactState(opts.providerState, {
|
|
1455
|
-
model: useModel,
|
|
1456
|
-
nativePrefix,
|
|
1457
|
-
responseId: result.responseId || null,
|
|
1458
|
-
}),
|
|
1459
|
-
};
|
|
1460
|
-
}
|
|
1461
1246
|
async ensureAuth({ forceRefresh = false, reason = 'preemptive' } = {}) {
|
|
1462
1247
|
if (!this.tokens) this.tokens = loadTokens();
|
|
1463
1248
|
if (!this.tokens)
|
|
1464
|
-
throw new Error('OpenAI OAuth not authenticated.
|
|
1465
|
-
// Pick up
|
|
1466
|
-
//
|
|
1467
|
-
//
|
|
1249
|
+
throw new Error('OpenAI OAuth not authenticated. Open /providers in mixdog to sign in.');
|
|
1250
|
+
// Pick up Mixdog-owned token updates the moment the auth file is
|
|
1251
|
+
// rewritten — without this, a fresh login is ignored until the in-memory
|
|
1252
|
+
// token hits its expiry skew.
|
|
1468
1253
|
const diskMtime = _tokensMaxMtime();
|
|
1469
|
-
// Watermark guards termination: if the
|
|
1470
|
-
//
|
|
1471
|
-
//
|
|
1472
|
-
// check can't re-fire on every ensureAuth().
|
|
1254
|
+
// Watermark guards termination: if the rewritten file is temporarily
|
|
1255
|
+
// unreadable/partial, record the scanned mtime so this check can't
|
|
1256
|
+
// re-fire on every ensureAuth().
|
|
1473
1257
|
if (diskMtime > 0 && diskMtime > (this._lastDiskScan || 0) && diskMtime > (this.tokens._mtimeMs || 0)) {
|
|
1474
1258
|
const fresh = loadTokens();
|
|
1475
1259
|
if (fresh?.access_token) {
|
|
@@ -1526,30 +1310,16 @@ export class OpenAIOAuthProvider {
|
|
|
1526
1310
|
this._refreshFallbackUntil = Date.now() + TOKEN_REFRESH_SKEW_MS;
|
|
1527
1311
|
return latest;
|
|
1528
1312
|
}
|
|
1529
|
-
throw new Error('OpenAI OAuth refresh token not available.
|
|
1313
|
+
throw new Error('OpenAI OAuth refresh token not available. Open /providers in mixdog to sign in again.');
|
|
1530
1314
|
}
|
|
1531
1315
|
|
|
1532
1316
|
try {
|
|
1533
1317
|
const _refreshT0 = Date.now();
|
|
1534
1318
|
const _expiringInMs = (latest?.expires_at ?? 0) - Date.now();
|
|
1535
|
-
if (process.env.
|
|
1319
|
+
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] auth-refresh-needed expiringInMs=${_expiringInMs}\n`); }
|
|
1536
1320
|
process.stderr.write(`[openai-oauth] Token ${reason}, refreshing...\n`);
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
refreshed = await refreshTokens(latest.refresh_token);
|
|
1540
|
-
} catch (refreshErr) {
|
|
1541
|
-
// invalid_grant: the Codex CLI rotated this single-use refresh
|
|
1542
|
-
// token between our disk read and this refresh. Re-read both
|
|
1543
|
-
// stores and retry ONCE with the freshest different token.
|
|
1544
|
-
if (!refreshErr?.isInvalidGrant) throw refreshErr;
|
|
1545
|
-
process.stderr.write('[openai-oauth] invalid_grant — re-reading disk, retrying refresh\n');
|
|
1546
|
-
const candidates = [_loadOwnCodexTokens(), _loadCodexCliTokens()].filter(Boolean)
|
|
1547
|
-
.sort((a, b) => (b._mtimeMs || 0) - (a._mtimeMs || 0));
|
|
1548
|
-
const freshTok = candidates.find(c => c.refresh_token && c.refresh_token !== latest.refresh_token);
|
|
1549
|
-
if (!freshTok) throw refreshErr;
|
|
1550
|
-
refreshed = await refreshTokens(freshTok.refresh_token);
|
|
1551
|
-
}
|
|
1552
|
-
if (process.env.MIXDOG_DEBUG_BRIDGE) { process.stderr.write(`[bridge-trace] auth-refresh-done elapsed=${Date.now() - _refreshT0}ms ok=${!!refreshed}\n`); }
|
|
1321
|
+
const refreshed = await refreshTokens(latest.refresh_token);
|
|
1322
|
+
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] auth-refresh-done elapsed=${Date.now() - _refreshT0}ms ok=${!!refreshed}\n`); }
|
|
1553
1323
|
if (!refreshed) throw new Error('refresh returned null');
|
|
1554
1324
|
process.stderr.write(`[openai-oauth] Token refreshed, expires in ${Math.round(((refreshed.expires_at || Date.now()) - Date.now()) / 1000)}s\n`);
|
|
1555
1325
|
return refreshed;
|
|
@@ -1561,7 +1331,7 @@ export class OpenAIOAuthProvider {
|
|
|
1561
1331
|
process.stderr.write(`[openai-oauth] Refresh failed (${msg}); using still-valid current token\n`);
|
|
1562
1332
|
return latest;
|
|
1563
1333
|
}
|
|
1564
|
-
throw new Error(`OpenAI OAuth token refresh failed (${msg}).
|
|
1334
|
+
throw new Error(`OpenAI OAuth token refresh failed (${msg}). Re-authenticate via provider login.`);
|
|
1565
1335
|
}
|
|
1566
1336
|
})().finally(() => { _oauthRefreshInFlight = null; });
|
|
1567
1337
|
|
|
@@ -1569,6 +1339,10 @@ export class OpenAIOAuthProvider {
|
|
|
1569
1339
|
return this.tokens;
|
|
1570
1340
|
}
|
|
1571
1341
|
async send(messages, model, tools, sendOpts) {
|
|
1342
|
+
// Re-warm a kept-alive socket before the turn (TTL-gated no-op while
|
|
1343
|
+
// hot). After an idle gap it re-opens one in parallel with auth/body
|
|
1344
|
+
// build so the HTTP/SSE path skips the cold TLS handshake.
|
|
1345
|
+
preconnect('https://chatgpt.com');
|
|
1572
1346
|
const opts = sendOpts || {};
|
|
1573
1347
|
const onStageChange = typeof opts.onStageChange === 'function' ? opts.onStageChange : null;
|
|
1574
1348
|
const onStreamDelta = typeof opts.onStreamDelta === 'function' ? opts.onStreamDelta : null;
|
|
@@ -1577,7 +1351,7 @@ export class OpenAIOAuthProvider {
|
|
|
1577
1351
|
const externalSignal = opts.signal || null;
|
|
1578
1352
|
const _sendSessionId = opts.sessionId || '(none)';
|
|
1579
1353
|
const _sendRole = opts.role || '(none)';
|
|
1580
|
-
if (process.env.
|
|
1354
|
+
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] auth-start sessionHash=${createHash('sha256').update(String(_sendSessionId)).digest('hex').slice(0, 8)} role=${_sendRole} expiringInMs=${this.tokens?.expires_at ? this.tokens.expires_at - Date.now() : 'unknown'}\n`); }
|
|
1581
1355
|
// Build request body in parallel with auth resolution. ensureAuth is
|
|
1582
1356
|
// a no-op fast-path on cached tokens, but a refresh round-trip can
|
|
1583
1357
|
// take 300ms+; the body build (message serialisation) overlaps cleanly.
|
|
@@ -1597,11 +1371,10 @@ export class OpenAIOAuthProvider {
|
|
|
1597
1371
|
const _authP = this.ensureAuth();
|
|
1598
1372
|
let auth = await _authP;
|
|
1599
1373
|
const body = await _bodyP;
|
|
1600
|
-
const hasImageContent = messagesHaveImageContent(messages);
|
|
1601
1374
|
// poolKey ≠ cacheKey by design (see openai-oauth-ws.mjs header note).
|
|
1602
1375
|
// poolKey is per-session so parallel reviewer/worker callers each
|
|
1603
1376
|
// get their own socket bucket — a sibling cannot grab a mid-turn
|
|
1604
|
-
// entry and trip
|
|
1377
|
+
// entry and trip the backend's "No tool call found for function call
|
|
1605
1378
|
// output with call_id …" rejection. cacheKey is prefix-scoped
|
|
1606
1379
|
// (base namespace + model/system/tools hash) and feeds both
|
|
1607
1380
|
// `body.prompt_cache_key` and the handshake `session_id` header, so
|
|
@@ -1625,8 +1398,21 @@ export class OpenAIOAuthProvider {
|
|
|
1625
1398
|
}
|
|
1626
1399
|
return result;
|
|
1627
1400
|
};
|
|
1628
|
-
const
|
|
1629
|
-
|
|
1401
|
+
const httpFallbackActive = () => {
|
|
1402
|
+
if (this._forceHttpFallbackUntil > Date.now()) return true;
|
|
1403
|
+
if (this._forceHttpFallback || this._forceHttpFallbackUntil) {
|
|
1404
|
+
this._forceHttpFallback = false;
|
|
1405
|
+
this._forceHttpFallbackUntil = 0;
|
|
1406
|
+
}
|
|
1407
|
+
return false;
|
|
1408
|
+
};
|
|
1409
|
+
const markStickyHttpFallback = () => {
|
|
1410
|
+
const ttlMs = _envPositiveInt('MIXDOG_OPENAI_OAUTH_HTTP_FALLBACK_STICKY_MS', 60_000);
|
|
1411
|
+
this._forceHttpFallback = true;
|
|
1412
|
+
this._forceHttpFallbackUntil = Date.now() + ttlMs;
|
|
1413
|
+
};
|
|
1414
|
+
const dispatchHttp = async (reason, originalErr = null, { sticky = false } = {}) => {
|
|
1415
|
+
appendAgentTrace({
|
|
1630
1416
|
sessionId: poolKey,
|
|
1631
1417
|
iteration,
|
|
1632
1418
|
kind: 'transport_fallback',
|
|
@@ -1642,7 +1428,13 @@ export class OpenAIOAuthProvider {
|
|
|
1642
1428
|
error_classifier: originalErr?.retryClassifier || originalErr?.midstreamClassifier || null,
|
|
1643
1429
|
},
|
|
1644
1430
|
});
|
|
1645
|
-
|
|
1431
|
+
if (reason === 'forced') {
|
|
1432
|
+
if (_envFlag('MIXDOG_OPENAI_OAUTH_LOG_FORCED_FALLBACK', false)) {
|
|
1433
|
+
process.stderr.write('[openai-oauth] WebSocket bypassed (forced); using HTTP/SSE\n');
|
|
1434
|
+
}
|
|
1435
|
+
} else {
|
|
1436
|
+
process.stderr.write(`[openai-oauth] WebSocket unhealthy (${reason}); falling back to HTTP/SSE\n`);
|
|
1437
|
+
}
|
|
1646
1438
|
const result = await sendHttp({
|
|
1647
1439
|
auth,
|
|
1648
1440
|
body,
|
|
@@ -1658,9 +1450,9 @@ export class OpenAIOAuthProvider {
|
|
|
1658
1450
|
useModel,
|
|
1659
1451
|
fetchFn: opts._fetchFn,
|
|
1660
1452
|
});
|
|
1661
|
-
|
|
1662
|
-
if (process.env.
|
|
1663
|
-
process.stderr.write(`[
|
|
1453
|
+
if (sticky) markStickyHttpFallback();
|
|
1454
|
+
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
1455
|
+
process.stderr.write(`[agent-trace] provider-send-end elapsed=${Date.now() - _t1}ms result=ok transport=http-fallback\n`);
|
|
1664
1456
|
}
|
|
1665
1457
|
return recordLiveModel(result);
|
|
1666
1458
|
};
|
|
@@ -1681,18 +1473,17 @@ export class OpenAIOAuthProvider {
|
|
|
1681
1473
|
forceFresh,
|
|
1682
1474
|
});
|
|
1683
1475
|
if (opts.forceHttpFallback === true
|
|
1684
|
-
||
|
|
1685
|
-
|| hasImageContent
|
|
1476
|
+
|| httpFallbackActive()
|
|
1686
1477
|
|| _envFlag('MIXDOG_OPENAI_OAUTH_FORCE_HTTP_FALLBACK', false)) {
|
|
1687
|
-
return dispatchHttp(
|
|
1478
|
+
return dispatchHttp('forced');
|
|
1688
1479
|
}
|
|
1689
1480
|
|
|
1690
1481
|
// Prefer WebSocket for hot cache/delta transport; fall back to HTTP/SSE
|
|
1691
1482
|
// after retry-exhausted handshake/acquire/no-first-event failures.
|
|
1692
1483
|
try {
|
|
1693
|
-
if (process.env.
|
|
1484
|
+
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] provider-send-start model=${useModel} role=${_sendRole} sessionHash=${createHash('sha256').update(String(_sendSessionId)).digest('hex').slice(0, 8)} iteration=${iteration ?? '(none)'}\n`); }
|
|
1694
1485
|
const result = await dispatchWs(false);
|
|
1695
|
-
if (process.env.
|
|
1486
|
+
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] provider-send-end elapsed=${Date.now() - _t1}ms result=ok\n`); }
|
|
1696
1487
|
return recordLiveModel(result);
|
|
1697
1488
|
} catch (err) {
|
|
1698
1489
|
const status = err?.httpStatus;
|
|
@@ -1705,17 +1496,21 @@ export class OpenAIOAuthProvider {
|
|
|
1705
1496
|
const liveTextEmitted = err?.liveTextEmitted === true || err?.unsafeToRetry === true;
|
|
1706
1497
|
if ((status === 401 || status === 403) && !liveTextEmitted) {
|
|
1707
1498
|
process.stderr.write(`[openai-oauth-ws] ${status} — forcing refresh and retrying once over WS\n`);
|
|
1708
|
-
if (process.env.
|
|
1499
|
+
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] provider-${status}-retry attempt=1\n`); }
|
|
1709
1500
|
this._refreshFallbackUntil = 0;
|
|
1710
1501
|
auth = await this.ensureAuth({ forceRefresh: true, reason: String(status) });
|
|
1711
1502
|
try {
|
|
1712
1503
|
const result = await dispatchWs(true);
|
|
1713
|
-
if (process.env.
|
|
1504
|
+
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] provider-send-end elapsed=${Date.now() - _t1}ms result=ok\n`); }
|
|
1714
1505
|
return recordLiveModel(result);
|
|
1715
1506
|
} catch (retryErr) {
|
|
1716
1507
|
if (_shouldUseOpenAIHttpFallback(retryErr, externalSignal)) {
|
|
1717
1508
|
try {
|
|
1718
|
-
return await dispatchHttp(
|
|
1509
|
+
return await dispatchHttp(
|
|
1510
|
+
retryErr?.retryClassifier || retryErr?.code || retryErr?.message || 'ws_auth_retry_failed',
|
|
1511
|
+
retryErr,
|
|
1512
|
+
{ sticky: true },
|
|
1513
|
+
);
|
|
1719
1514
|
} catch (fallbackErr) {
|
|
1720
1515
|
try { retryErr.fallbackError = fallbackErr; } catch {}
|
|
1721
1516
|
throw retryErr;
|
|
@@ -1738,7 +1533,11 @@ export class OpenAIOAuthProvider {
|
|
|
1738
1533
|
}
|
|
1739
1534
|
if (_shouldUseOpenAIHttpFallback(err, externalSignal)) {
|
|
1740
1535
|
try {
|
|
1741
|
-
return await dispatchHttp(
|
|
1536
|
+
return await dispatchHttp(
|
|
1537
|
+
err?.retryClassifier || err?.midstreamClassifier || err?.code || err?.message || 'ws_failed',
|
|
1538
|
+
err,
|
|
1539
|
+
{ sticky: true },
|
|
1540
|
+
);
|
|
1742
1541
|
} catch (fallbackErr) {
|
|
1743
1542
|
try { err.fallbackError = fallbackErr; } catch {}
|
|
1744
1543
|
throw err;
|
|
@@ -1748,7 +1547,7 @@ export class OpenAIOAuthProvider {
|
|
|
1748
1547
|
}
|
|
1749
1548
|
}
|
|
1750
1549
|
async listModels() {
|
|
1751
|
-
// Dynamic lookup via
|
|
1550
|
+
// Dynamic lookup via /backend-api/codex/models. Cached 24h.
|
|
1752
1551
|
// Endpoint returns rich metadata (context_window, reasoning levels,
|
|
1753
1552
|
// visibility) that is more detailed than /v1/models.
|
|
1754
1553
|
const cached = await _loadCodexModelCache();
|
|
@@ -1772,7 +1571,7 @@ export class OpenAIOAuthProvider {
|
|
|
1772
1571
|
},
|
|
1773
1572
|
dispatcher: getLlmDispatcher(),
|
|
1774
1573
|
});
|
|
1775
|
-
if (!res.ok) throw new Error(`
|
|
1574
|
+
if (!res.ok) throw new Error(`openai-oauth list_models ${res.status}`);
|
|
1776
1575
|
const data = await res.json();
|
|
1777
1576
|
const items = Array.isArray(data?.models) ? data.models : [];
|
|
1778
1577
|
const normalized = items.map(m => _normalizeCodexModel(m));
|
|
@@ -1785,7 +1584,7 @@ export class OpenAIOAuthProvider {
|
|
|
1785
1584
|
_lastCodexListModelsError = err?.message || String(err);
|
|
1786
1585
|
process.stderr.write(`[openai-oauth] listModels fetch failed (${_lastCodexListModelsError})\n`);
|
|
1787
1586
|
// No fallback catalog — empty list signals the UI to show a
|
|
1788
|
-
// "catalog unavailable, retry" state.
|
|
1587
|
+
// "catalog unavailable, retry" state. openai-oauth has no equivalent to
|
|
1789
1588
|
// Anthropic's family tokens so there's no meaningful minimal list.
|
|
1790
1589
|
return [];
|
|
1791
1590
|
}
|
|
@@ -1857,7 +1656,64 @@ function _scrubOAuthLoginBody(text) {
|
|
|
1857
1656
|
.replace(/[A-Za-z0-9_-]{32,}\.[A-Za-z0-9._-]+/g, '[REDACTED]');
|
|
1858
1657
|
}
|
|
1859
1658
|
|
|
1860
|
-
|
|
1659
|
+
function _parseOAuthCodeInput(input) {
|
|
1660
|
+
const value = String(input || '').trim();
|
|
1661
|
+
if (!value) return { code: '', state: '' };
|
|
1662
|
+
try {
|
|
1663
|
+
const url = new URL(value);
|
|
1664
|
+
const code = url.searchParams.get('code') || '';
|
|
1665
|
+
const state = url.searchParams.get('state') || '';
|
|
1666
|
+
if (code || state) return { code, state };
|
|
1667
|
+
} catch { /* not a URL */ }
|
|
1668
|
+
if (value.includes('#')) {
|
|
1669
|
+
const [code, state] = value.split('#', 2);
|
|
1670
|
+
return { code: String(code || '').trim(), state: String(state || '').trim() };
|
|
1671
|
+
}
|
|
1672
|
+
if (value.includes('code=')) {
|
|
1673
|
+
const params = new URLSearchParams(value.startsWith('?') ? value.slice(1) : value);
|
|
1674
|
+
return { code: params.get('code') || '', state: params.get('state') || '' };
|
|
1675
|
+
}
|
|
1676
|
+
return { code: value, state: '' };
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
async function exchangeAuthorizationCode({ pkce, code }) {
|
|
1680
|
+
const cleanCode = String(code || '').trim();
|
|
1681
|
+
if (!cleanCode) throw new Error('[openai-oauth] authorization code is required');
|
|
1682
|
+
const tokenRes = await fetch(TOKEN_URL, {
|
|
1683
|
+
method: 'POST',
|
|
1684
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
1685
|
+
body: new URLSearchParams({
|
|
1686
|
+
grant_type: 'authorization_code',
|
|
1687
|
+
code: cleanCode,
|
|
1688
|
+
redirect_uri: REDIRECT_URI,
|
|
1689
|
+
client_id: CLIENT_ID,
|
|
1690
|
+
code_verifier: pkce.verifier,
|
|
1691
|
+
}),
|
|
1692
|
+
redirect: 'error',
|
|
1693
|
+
signal: AbortSignal.timeout(TOKEN_TIMEOUT_MS),
|
|
1694
|
+
});
|
|
1695
|
+
if (!tokenRes.ok) {
|
|
1696
|
+
const text = await tokenRes.text().catch(() => '');
|
|
1697
|
+
throw new Error(`[openai-oauth] token exchange ${tokenRes.status}: ${_scrubOAuthLoginBody(text).slice(0, 500)}`);
|
|
1698
|
+
}
|
|
1699
|
+
const json = await tokenRes.json();
|
|
1700
|
+
if (!json.access_token || !json.refresh_token) {
|
|
1701
|
+
throw new Error('[openai-oauth] token exchange response missing access_token or refresh_token');
|
|
1702
|
+
}
|
|
1703
|
+
const expiresAt = (typeof json.expires_in === 'number'
|
|
1704
|
+
? Date.now() + json.expires_in * 1000
|
|
1705
|
+
: 0) || _expiryFromAccessToken(json.access_token);
|
|
1706
|
+
const tokens = {
|
|
1707
|
+
access_token: json.access_token,
|
|
1708
|
+
refresh_token: json.refresh_token,
|
|
1709
|
+
expires_at: expiresAt,
|
|
1710
|
+
account_id: extractAccountId(json.access_token),
|
|
1711
|
+
};
|
|
1712
|
+
saveTokens(tokens);
|
|
1713
|
+
return tokens;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
export async function beginOAuthLogin() {
|
|
1861
1717
|
const pkce = generatePKCE();
|
|
1862
1718
|
const state = randomBytes(16).toString('hex');
|
|
1863
1719
|
const url = new URL(AUTHORIZE_URL);
|
|
@@ -1872,18 +1728,20 @@ export async function loginOAuth() {
|
|
|
1872
1728
|
url.searchParams.set('state', state);
|
|
1873
1729
|
url.searchParams.set('originator', CODEX_OAUTH_ORIGINATOR);
|
|
1874
1730
|
|
|
1875
|
-
|
|
1731
|
+
let server = null;
|
|
1732
|
+
let timeout = null;
|
|
1733
|
+
let finish = null;
|
|
1734
|
+
const waitForCallback = new Promise((resolve, reject) => {
|
|
1876
1735
|
let settled = false;
|
|
1877
|
-
|
|
1878
|
-
const settle = (value, error = null) => {
|
|
1736
|
+
finish = (value, error = null) => {
|
|
1879
1737
|
if (settled) return;
|
|
1880
1738
|
settled = true;
|
|
1881
1739
|
if (timeout) clearTimeout(timeout);
|
|
1882
|
-
try { server
|
|
1740
|
+
try { server?.close(); } catch { /* already closed */ }
|
|
1883
1741
|
if (error) reject(error);
|
|
1884
1742
|
else resolve(value);
|
|
1885
1743
|
};
|
|
1886
|
-
|
|
1744
|
+
server = createServer(async (req, res) => {
|
|
1887
1745
|
const u = new URL(req.url || '/', `http://${CALLBACK_HOST}:${CALLBACK_PORT}`);
|
|
1888
1746
|
if (u.pathname !== CALLBACK_PATH) {
|
|
1889
1747
|
res.writeHead(404);
|
|
@@ -1894,53 +1752,21 @@ export async function loginOAuth() {
|
|
|
1894
1752
|
if (!code || u.searchParams.get('state') !== state) {
|
|
1895
1753
|
res.writeHead(400);
|
|
1896
1754
|
res.end('Invalid');
|
|
1897
|
-
|
|
1755
|
+
finish(null);
|
|
1898
1756
|
return;
|
|
1899
1757
|
}
|
|
1900
1758
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1901
|
-
res.end('<html><body><h2>
|
|
1759
|
+
res.end('<html><body><h2>OpenAI OAuth login successful! You can close this tab.</h2></body></html>');
|
|
1902
1760
|
try {
|
|
1903
|
-
const
|
|
1904
|
-
|
|
1905
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
1906
|
-
body: new URLSearchParams({
|
|
1907
|
-
grant_type: 'authorization_code',
|
|
1908
|
-
code,
|
|
1909
|
-
redirect_uri: REDIRECT_URI,
|
|
1910
|
-
client_id: CLIENT_ID,
|
|
1911
|
-
code_verifier: pkce.verifier,
|
|
1912
|
-
}),
|
|
1913
|
-
redirect: 'error',
|
|
1914
|
-
signal: AbortSignal.timeout(TOKEN_TIMEOUT_MS),
|
|
1915
|
-
});
|
|
1916
|
-
if (!tokenRes.ok) {
|
|
1917
|
-
const text = await tokenRes.text().catch(() => '');
|
|
1918
|
-
settle(null, new Error(`[openai-oauth] token exchange ${tokenRes.status}: ${_scrubOAuthLoginBody(text).slice(0, 500)}`));
|
|
1919
|
-
return;
|
|
1920
|
-
}
|
|
1921
|
-
const json = await tokenRes.json();
|
|
1922
|
-
if (!json.access_token || !json.refresh_token) {
|
|
1923
|
-
settle(null, new Error('[openai-oauth] token exchange response missing access_token or refresh_token'));
|
|
1924
|
-
return;
|
|
1925
|
-
}
|
|
1926
|
-
const expiresAt = (typeof json.expires_in === 'number'
|
|
1927
|
-
? Date.now() + json.expires_in * 1000
|
|
1928
|
-
: 0) || _expiryFromAccessToken(json.access_token);
|
|
1929
|
-
const tokens = {
|
|
1930
|
-
access_token: json.access_token,
|
|
1931
|
-
refresh_token: json.refresh_token,
|
|
1932
|
-
expires_at: expiresAt,
|
|
1933
|
-
account_id: extractAccountId(json.access_token),
|
|
1934
|
-
};
|
|
1935
|
-
saveTokens(tokens);
|
|
1936
|
-
settle(tokens);
|
|
1761
|
+
const tokens = await exchangeAuthorizationCode({ pkce, code });
|
|
1762
|
+
finish(tokens);
|
|
1937
1763
|
} catch (err) {
|
|
1938
|
-
|
|
1764
|
+
finish(null, err instanceof Error ? err : new Error(String(err)));
|
|
1939
1765
|
}
|
|
1940
1766
|
});
|
|
1941
|
-
timeout = setTimeout(() =>
|
|
1767
|
+
timeout = setTimeout(() => finish(null), LOGIN_TIMEOUT_MS);
|
|
1942
1768
|
server.listen(CALLBACK_PORT, CALLBACK_HOST, async () => {
|
|
1943
|
-
process.stderr.write(`\n[openai-oauth] Open this URL to log in to ChatGPT (
|
|
1769
|
+
process.stderr.write(`\n[openai-oauth] Open this URL to log in to ChatGPT (OpenAI OAuth):\n${url.toString()}\n\n`);
|
|
1944
1770
|
try {
|
|
1945
1771
|
const { openInBrowser } = await import('../../../shared/open-url.mjs');
|
|
1946
1772
|
openInBrowser(url.toString());
|
|
@@ -1948,6 +1774,27 @@ export async function loginOAuth() {
|
|
|
1948
1774
|
process.stderr.write(`[openai-oauth] browser open failed: ${String(err?.message || err).slice(0, 200)}\n`);
|
|
1949
1775
|
}
|
|
1950
1776
|
});
|
|
1951
|
-
server.on('error', (err) =>
|
|
1777
|
+
server.on('error', (err) => finish(null, new Error(`[openai-oauth] callback server failed on ${CALLBACK_HOST}:${CALLBACK_PORT}: ${err?.message || err}`)));
|
|
1952
1778
|
});
|
|
1779
|
+
|
|
1780
|
+
return {
|
|
1781
|
+
provider: 'openai-oauth',
|
|
1782
|
+
url: url.toString(),
|
|
1783
|
+
waitForCallback,
|
|
1784
|
+
completeCode: async (input) => {
|
|
1785
|
+
const parsed = _parseOAuthCodeInput(input);
|
|
1786
|
+
if (parsed.state && parsed.state !== state) throw new Error('[openai-oauth] OAuth state mismatch');
|
|
1787
|
+
const tokens = await exchangeAuthorizationCode({ pkce, code: parsed.code });
|
|
1788
|
+
finish?.(tokens);
|
|
1789
|
+
return tokens;
|
|
1790
|
+
},
|
|
1791
|
+
cancel: () => {
|
|
1792
|
+
finish?.(null);
|
|
1793
|
+
},
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
export async function loginOAuth() {
|
|
1798
|
+
const login = await beginOAuthLogin();
|
|
1799
|
+
return await login.waitForCallback;
|
|
1953
1800
|
}
|