mixdog 0.8.1 → 0.9.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/README.md +47 -23
- package/package.json +34 -28
- 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 +205 -33
- 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 +1222 -90
- 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 -29
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +242 -98
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2211 -446
- 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 +108 -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 +380 -268
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -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 +203 -71
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
- 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 +289 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- 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 +840 -612
- package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
- 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 +35 -8
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
- 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.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 +149 -51
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +0 -1
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
- package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -6
- package/src/runtime/channels/lib/tool-format.mjs +0 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/lib/keychain-cjs.cjs +0 -1
- package/src/runtime/memory/data/runtime-manifest.json +6 -7
- package/src/runtime/memory/index.mjs +519 -118
- 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/llm-worker-host.mjs +0 -4
- package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
- package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
- package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -2
- 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 +1 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +80 -21
- package/src/runtime/memory/lib/session-ingest.mjs +196 -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 +104 -473
- package/src/runtime/search/lib/config.mjs +0 -4
- package/src/runtime/search/lib/state.mjs +1 -15
- package/src/runtime/search/lib/web-tools.mjs +44 -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 +139 -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 -95
- 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 +1 -78
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4800 -2140
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +14 -4
- 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 +428 -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 +597 -362
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +499 -0
- package/src/tui/components/tool-output-format.test.mjs +312 -0
- package/src/tui/dist/index.mjs +9192 -2399
- 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 +671 -69
- package/src/tui/markdown/format-token.test.mjs +312 -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 +756 -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 +2 -13
- package/src/vendor/statusline/bin/statusline-lib.mjs +1 -620
- 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/ink.js +54 -8
- 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/hooks/lib/permission-rules.cjs +0 -170
- package/src/hooks/lib/settings-loader.cjs +0 -112
- package/src/lib/hook-pipe-path.cjs +0 -10
- 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/channels/lib/hook-pipe-server.mjs +0 -671
- 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,38 +21,42 @@ 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,
|
|
35
34
|
createTimeoutSignal,
|
|
36
35
|
} from '../stall-policy.mjs';
|
|
37
|
-
import { populateHttpStatusFromMessage } from './retry-classifier.mjs';
|
|
36
|
+
import { populateHttpStatusFromMessage, shouldFallbackTransport } 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
|
|
|
@@ -885,29 +742,14 @@ function _buildOpenAIHttpFallbackHeaders({ auth, cacheKey }) {
|
|
|
885
742
|
return headers;
|
|
886
743
|
}
|
|
887
744
|
|
|
745
|
+
// WS→HTTP/SSE fallback predicate → shared shouldFallbackTransport
|
|
746
|
+
// (retry-classifier.mjs). The per-provider env flag is computed here and passed
|
|
747
|
+
// as `enabled`; the deny-order + allow-list are identical to the former copy.
|
|
888
748
|
function _shouldUseOpenAIHttpFallback(err, externalSignal) {
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
// concatenate a second attempt onto rendered output. Never fall back.
|
|
894
|
-
if (err?.liveTextEmitted === true) return false;
|
|
895
|
-
const status = Number(err?.httpStatus || err?.status || 0);
|
|
896
|
-
if (status === 401 || status === 403 || status === 404 || status === 429) return false;
|
|
897
|
-
if (status >= 500 && status < 600) return true;
|
|
898
|
-
const code = String(err?.code || '');
|
|
899
|
-
if (['EWSACQUIRETIMEOUT', 'ETIMEDOUT', 'ESOCKETTIMEDOUT', 'ECONNRESET', 'EAI_AGAIN', 'ENOTFOUND', 'EAI_NODATA', 'ECONNREFUSED', 'ENETUNREACH', 'EHOSTUNREACH', 'EPIPE'].includes(code)) {
|
|
900
|
-
return true;
|
|
901
|
-
}
|
|
902
|
-
const classifier = String(err?.retryClassifier || err?.midstreamClassifier || '');
|
|
903
|
-
if (['timeout', 'reset', 'dns', 'refused', 'network', 'acquire_timeout', 'http_5xx', 'first_byte_timeout', 'first_meaningful_timeout'].includes(classifier)) {
|
|
904
|
-
return true;
|
|
905
|
-
}
|
|
906
|
-
if (/^http_5\d\d$/.test(classifier)) return true;
|
|
907
|
-
if (err?.firstByteTimeout) return true;
|
|
908
|
-
if (err?.firstMeaningfulTimeout) return true;
|
|
909
|
-
const msg = String(err?.message || '');
|
|
910
|
-
return /opening handshake has timed out|socket hang up|acquire timed out|no first server event|no meaningful output/i.test(msg);
|
|
749
|
+
return shouldFallbackTransport(err, {
|
|
750
|
+
signal: externalSignal,
|
|
751
|
+
enabled: _envFlag('MIXDOG_OPENAI_OAUTH_HTTP_FALLBACK', true),
|
|
752
|
+
});
|
|
911
753
|
}
|
|
912
754
|
|
|
913
755
|
// Exported for the single-emit regression smoke (scripts/openai-oauth-
|
|
@@ -960,7 +802,7 @@ export async function sendViaHttpSse({
|
|
|
960
802
|
headerTimeout.cleanup();
|
|
961
803
|
}
|
|
962
804
|
|
|
963
|
-
|
|
805
|
+
traceAgentFetch({
|
|
964
806
|
sessionId: poolKey,
|
|
965
807
|
headersMs: Date.now() - fetchStartedAt,
|
|
966
808
|
httpStatus: response.status,
|
|
@@ -1019,7 +861,6 @@ export async function sendViaHttpSse({
|
|
|
1019
861
|
const toolCalls = [];
|
|
1020
862
|
const pendingCalls = new Map();
|
|
1021
863
|
const reasoningItems = [];
|
|
1022
|
-
const compactionItems = [];
|
|
1023
864
|
const citations = [];
|
|
1024
865
|
const citationKeys = new Set();
|
|
1025
866
|
const webSearchCalls = [];
|
|
@@ -1067,10 +908,34 @@ export async function sendViaHttpSse({
|
|
|
1067
908
|
});
|
|
1068
909
|
}
|
|
1069
910
|
};
|
|
1070
|
-
const
|
|
1071
|
-
if (!item ||
|
|
1072
|
-
|
|
1073
|
-
|
|
911
|
+
const pushToolSearchCall = (item) => {
|
|
912
|
+
if (!item || item.type !== 'tool_search_call') return;
|
|
913
|
+
const callId = item.call_id || item.id || '';
|
|
914
|
+
if (!callId || toolCalls.some(t => t.id === callId)) return;
|
|
915
|
+
let args = {};
|
|
916
|
+
if (item.arguments && typeof item.arguments === 'object') {
|
|
917
|
+
args = item.arguments;
|
|
918
|
+
} else if (typeof item.arguments === 'string' && item.arguments.trim()) {
|
|
919
|
+
// Non-empty but malformed tool_search arguments are deterministic
|
|
920
|
+
// bad JSON (the item is only emitted on completion). Surface an
|
|
921
|
+
// invalid-args marker instead of swallowing to {} so the model can
|
|
922
|
+
// self-correct in the same turn.
|
|
923
|
+
args = _parseJsonObject(item.arguments);
|
|
924
|
+
}
|
|
925
|
+
const call = {
|
|
926
|
+
id: callId,
|
|
927
|
+
name: 'tool_search',
|
|
928
|
+
arguments: args,
|
|
929
|
+
nativeType: 'tool_search_call',
|
|
930
|
+
};
|
|
931
|
+
toolCalls.push(call);
|
|
932
|
+
emitToolCall(call);
|
|
933
|
+
};
|
|
934
|
+
const pushCustomToolCall = (item) => {
|
|
935
|
+
const call = customToolCallFromResponseItem(item);
|
|
936
|
+
if (!call || toolCalls.some(t => t.id === call.id)) return;
|
|
937
|
+
toolCalls.push(call);
|
|
938
|
+
emitToolCall(call);
|
|
1074
939
|
};
|
|
1075
940
|
const meaningful = () => {
|
|
1076
941
|
if (ttftMs == null) ttftMs = Date.now() - sseStartedAt;
|
|
@@ -1123,11 +988,13 @@ export async function sendViaHttpSse({
|
|
|
1123
988
|
meaningful();
|
|
1124
989
|
break;
|
|
1125
990
|
}
|
|
991
|
+
case 'response.custom_tool_call_input.delta':
|
|
992
|
+
meaningful();
|
|
993
|
+
break;
|
|
1126
994
|
case 'response.output_item.done': {
|
|
1127
995
|
const item = event.item || {};
|
|
1128
996
|
pushReasoningItem(item);
|
|
1129
997
|
pushWebSearchCall(item);
|
|
1130
|
-
pushCompactionItem(item);
|
|
1131
998
|
if (item.type === 'function_call') {
|
|
1132
999
|
const tc = toolCalls.find(t => t._pendingItemId === (item.id || ''));
|
|
1133
1000
|
if (tc) {
|
|
@@ -1138,6 +1005,11 @@ export async function sendViaHttpSse({
|
|
|
1138
1005
|
emitToolCall(tc);
|
|
1139
1006
|
}
|
|
1140
1007
|
}
|
|
1008
|
+
} else if (item.type === 'tool_search_call') {
|
|
1009
|
+
pushToolSearchCall(item);
|
|
1010
|
+
} else if (item.type === 'custom_tool_call') {
|
|
1011
|
+
pushCustomToolCall(item);
|
|
1012
|
+
meaningful();
|
|
1141
1013
|
}
|
|
1142
1014
|
break;
|
|
1143
1015
|
}
|
|
@@ -1165,8 +1037,11 @@ export async function sendViaHttpSse({
|
|
|
1165
1037
|
pushReasoningItem(item);
|
|
1166
1038
|
} else if (item.type === 'web_search_call') {
|
|
1167
1039
|
pushWebSearchCall(item);
|
|
1168
|
-
} else if (
|
|
1169
|
-
|
|
1040
|
+
} else if (item.type === 'tool_search_call') {
|
|
1041
|
+
pushToolSearchCall(item);
|
|
1042
|
+
} else if (item.type === 'custom_tool_call') {
|
|
1043
|
+
pushCustomToolCall(item);
|
|
1044
|
+
meaningful();
|
|
1170
1045
|
} else if (item.type === 'function_call') {
|
|
1171
1046
|
// Match the still-pending placeholder by item id, or
|
|
1172
1047
|
// an already-recorded call by its canonical call_id —
|
|
@@ -1282,15 +1157,12 @@ export async function sendViaHttpSse({
|
|
|
1282
1157
|
if (unresolved) {
|
|
1283
1158
|
throw new Error(`OpenAI OAuth HTTP fallback function_call salvage failed: missing call_id/name for item_id=${unresolved._pendingItemId || '?'}`);
|
|
1284
1159
|
}
|
|
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
1160
|
if (!completed && !content && !toolCalls.length) {
|
|
1289
1161
|
throw new Error('OpenAI OAuth HTTP fallback ended before response.completed');
|
|
1290
1162
|
}
|
|
1291
1163
|
|
|
1292
1164
|
const liveModel = model || useModel;
|
|
1293
|
-
|
|
1165
|
+
traceAgentSse({
|
|
1294
1166
|
sessionId: poolKey,
|
|
1295
1167
|
sseParseMs: Date.now() - sseStartedAt,
|
|
1296
1168
|
ttftMs,
|
|
@@ -1299,7 +1171,7 @@ export async function sendViaHttpSse({
|
|
|
1299
1171
|
transport: 'sse',
|
|
1300
1172
|
});
|
|
1301
1173
|
if (usage) {
|
|
1302
|
-
|
|
1174
|
+
traceAgentUsage({
|
|
1303
1175
|
sessionId: poolKey,
|
|
1304
1176
|
iteration,
|
|
1305
1177
|
inputTokens: usage.inputTokens || 0,
|
|
@@ -1318,8 +1190,6 @@ export async function sendViaHttpSse({
|
|
|
1318
1190
|
content,
|
|
1319
1191
|
model: liveModel,
|
|
1320
1192
|
reasoningItems: reasoningItems.length ? reasoningItems : undefined,
|
|
1321
|
-
compactionItem: compactionItems.length === 1 ? compactionItems[0] : undefined,
|
|
1322
|
-
compactionItems: compactionItems.length ? compactionItems : undefined,
|
|
1323
1193
|
toolCalls: toolCalls.length ? toolCalls.map(({ _pendingItemId, ...t }) => t) : undefined,
|
|
1324
1194
|
citations: citations.length ? citations : undefined,
|
|
1325
1195
|
webSearchCalls: webSearchCalls.length ? webSearchCalls : undefined,
|
|
@@ -1339,137 +1209,29 @@ export class OpenAIOAuthProvider {
|
|
|
1339
1209
|
tokens = null;
|
|
1340
1210
|
_refreshFallbackUntil = 0;
|
|
1341
1211
|
_forceHttpFallback = false;
|
|
1212
|
+
_forceHttpFallbackUntil = 0;
|
|
1342
1213
|
config;
|
|
1343
1214
|
constructor(config) {
|
|
1344
1215
|
this.config = config || {};
|
|
1345
1216
|
this.tokens = loadTokens();
|
|
1346
|
-
// Warm a kept-alive socket to the
|
|
1217
|
+
// Warm a kept-alive socket to the OAuth responses API so the first
|
|
1347
1218
|
// request skips the cold TLS handshake. Best-effort; never throws.
|
|
1348
1219
|
preconnect('https://chatgpt.com');
|
|
1349
1220
|
}
|
|
1350
1221
|
getCachedModelInfo(model) {
|
|
1351
1222
|
return _findCachedCodexModel(model);
|
|
1352
1223
|
}
|
|
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
1224
|
async ensureAuth({ forceRefresh = false, reason = 'preemptive' } = {}) {
|
|
1462
1225
|
if (!this.tokens) this.tokens = loadTokens();
|
|
1463
1226
|
if (!this.tokens)
|
|
1464
|
-
throw new Error('OpenAI OAuth not authenticated.
|
|
1465
|
-
// Pick up
|
|
1466
|
-
//
|
|
1467
|
-
//
|
|
1227
|
+
throw new Error('OpenAI OAuth not authenticated. Open /providers in mixdog to sign in.');
|
|
1228
|
+
// Pick up Mixdog-owned token updates the moment the auth file is
|
|
1229
|
+
// rewritten — without this, a fresh login is ignored until the in-memory
|
|
1230
|
+
// token hits its expiry skew.
|
|
1468
1231
|
const diskMtime = _tokensMaxMtime();
|
|
1469
|
-
// Watermark guards termination: if the
|
|
1470
|
-
//
|
|
1471
|
-
//
|
|
1472
|
-
// check can't re-fire on every ensureAuth().
|
|
1232
|
+
// Watermark guards termination: if the rewritten file is temporarily
|
|
1233
|
+
// unreadable/partial, record the scanned mtime so this check can't
|
|
1234
|
+
// re-fire on every ensureAuth().
|
|
1473
1235
|
if (diskMtime > 0 && diskMtime > (this._lastDiskScan || 0) && diskMtime > (this.tokens._mtimeMs || 0)) {
|
|
1474
1236
|
const fresh = loadTokens();
|
|
1475
1237
|
if (fresh?.access_token) {
|
|
@@ -1526,30 +1288,16 @@ export class OpenAIOAuthProvider {
|
|
|
1526
1288
|
this._refreshFallbackUntil = Date.now() + TOKEN_REFRESH_SKEW_MS;
|
|
1527
1289
|
return latest;
|
|
1528
1290
|
}
|
|
1529
|
-
throw new Error('OpenAI OAuth refresh token not available.
|
|
1291
|
+
throw new Error('OpenAI OAuth refresh token not available. Open /providers in mixdog to sign in again.');
|
|
1530
1292
|
}
|
|
1531
1293
|
|
|
1532
1294
|
try {
|
|
1533
1295
|
const _refreshT0 = Date.now();
|
|
1534
1296
|
const _expiringInMs = (latest?.expires_at ?? 0) - Date.now();
|
|
1535
|
-
if (process.env.
|
|
1297
|
+
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] auth-refresh-needed expiringInMs=${_expiringInMs}\n`); }
|
|
1536
1298
|
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`); }
|
|
1299
|
+
const refreshed = await refreshTokens(latest.refresh_token);
|
|
1300
|
+
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] auth-refresh-done elapsed=${Date.now() - _refreshT0}ms ok=${!!refreshed}\n`); }
|
|
1553
1301
|
if (!refreshed) throw new Error('refresh returned null');
|
|
1554
1302
|
process.stderr.write(`[openai-oauth] Token refreshed, expires in ${Math.round(((refreshed.expires_at || Date.now()) - Date.now()) / 1000)}s\n`);
|
|
1555
1303
|
return refreshed;
|
|
@@ -1561,7 +1309,7 @@ export class OpenAIOAuthProvider {
|
|
|
1561
1309
|
process.stderr.write(`[openai-oauth] Refresh failed (${msg}); using still-valid current token\n`);
|
|
1562
1310
|
return latest;
|
|
1563
1311
|
}
|
|
1564
|
-
throw new Error(`OpenAI OAuth token refresh failed (${msg}).
|
|
1312
|
+
throw new Error(`OpenAI OAuth token refresh failed (${msg}). Re-authenticate via provider login.`);
|
|
1565
1313
|
}
|
|
1566
1314
|
})().finally(() => { _oauthRefreshInFlight = null; });
|
|
1567
1315
|
|
|
@@ -1569,6 +1317,10 @@ export class OpenAIOAuthProvider {
|
|
|
1569
1317
|
return this.tokens;
|
|
1570
1318
|
}
|
|
1571
1319
|
async send(messages, model, tools, sendOpts) {
|
|
1320
|
+
// Re-warm a kept-alive socket before the turn (TTL-gated no-op while
|
|
1321
|
+
// hot). After an idle gap it re-opens one in parallel with auth/body
|
|
1322
|
+
// build so the HTTP/SSE path skips the cold TLS handshake.
|
|
1323
|
+
preconnect('https://chatgpt.com');
|
|
1572
1324
|
const opts = sendOpts || {};
|
|
1573
1325
|
const onStageChange = typeof opts.onStageChange === 'function' ? opts.onStageChange : null;
|
|
1574
1326
|
const onStreamDelta = typeof opts.onStreamDelta === 'function' ? opts.onStreamDelta : null;
|
|
@@ -1577,7 +1329,7 @@ export class OpenAIOAuthProvider {
|
|
|
1577
1329
|
const externalSignal = opts.signal || null;
|
|
1578
1330
|
const _sendSessionId = opts.sessionId || '(none)';
|
|
1579
1331
|
const _sendRole = opts.role || '(none)';
|
|
1580
|
-
if (process.env.
|
|
1332
|
+
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
1333
|
// Build request body in parallel with auth resolution. ensureAuth is
|
|
1582
1334
|
// a no-op fast-path on cached tokens, but a refresh round-trip can
|
|
1583
1335
|
// take 300ms+; the body build (message serialisation) overlaps cleanly.
|
|
@@ -1597,11 +1349,10 @@ export class OpenAIOAuthProvider {
|
|
|
1597
1349
|
const _authP = this.ensureAuth();
|
|
1598
1350
|
let auth = await _authP;
|
|
1599
1351
|
const body = await _bodyP;
|
|
1600
|
-
const hasImageContent = messagesHaveImageContent(messages);
|
|
1601
1352
|
// poolKey ≠ cacheKey by design (see openai-oauth-ws.mjs header note).
|
|
1602
1353
|
// poolKey is per-session so parallel reviewer/worker callers each
|
|
1603
1354
|
// get their own socket bucket — a sibling cannot grab a mid-turn
|
|
1604
|
-
// entry and trip
|
|
1355
|
+
// entry and trip the backend's "No tool call found for function call
|
|
1605
1356
|
// output with call_id …" rejection. cacheKey is prefix-scoped
|
|
1606
1357
|
// (base namespace + model/system/tools hash) and feeds both
|
|
1607
1358
|
// `body.prompt_cache_key` and the handshake `session_id` header, so
|
|
@@ -1625,8 +1376,21 @@ export class OpenAIOAuthProvider {
|
|
|
1625
1376
|
}
|
|
1626
1377
|
return result;
|
|
1627
1378
|
};
|
|
1628
|
-
const
|
|
1629
|
-
|
|
1379
|
+
const httpFallbackActive = () => {
|
|
1380
|
+
if (this._forceHttpFallbackUntil > Date.now()) return true;
|
|
1381
|
+
if (this._forceHttpFallback || this._forceHttpFallbackUntil) {
|
|
1382
|
+
this._forceHttpFallback = false;
|
|
1383
|
+
this._forceHttpFallbackUntil = 0;
|
|
1384
|
+
}
|
|
1385
|
+
return false;
|
|
1386
|
+
};
|
|
1387
|
+
const markStickyHttpFallback = () => {
|
|
1388
|
+
const ttlMs = _envPositiveInt('MIXDOG_OPENAI_OAUTH_HTTP_FALLBACK_STICKY_MS', 60_000);
|
|
1389
|
+
this._forceHttpFallback = true;
|
|
1390
|
+
this._forceHttpFallbackUntil = Date.now() + ttlMs;
|
|
1391
|
+
};
|
|
1392
|
+
const dispatchHttp = async (reason, originalErr = null, { sticky = false } = {}) => {
|
|
1393
|
+
appendAgentTrace({
|
|
1630
1394
|
sessionId: poolKey,
|
|
1631
1395
|
iteration,
|
|
1632
1396
|
kind: 'transport_fallback',
|
|
@@ -1642,7 +1406,13 @@ export class OpenAIOAuthProvider {
|
|
|
1642
1406
|
error_classifier: originalErr?.retryClassifier || originalErr?.midstreamClassifier || null,
|
|
1643
1407
|
},
|
|
1644
1408
|
});
|
|
1645
|
-
|
|
1409
|
+
if (reason === 'forced') {
|
|
1410
|
+
if (_envFlag('MIXDOG_OPENAI_OAUTH_LOG_FORCED_FALLBACK', false)) {
|
|
1411
|
+
process.stderr.write('[openai-oauth] WebSocket bypassed (forced); using HTTP/SSE\n');
|
|
1412
|
+
}
|
|
1413
|
+
} else {
|
|
1414
|
+
process.stderr.write(`[openai-oauth] WebSocket unhealthy (${reason}); falling back to HTTP/SSE\n`);
|
|
1415
|
+
}
|
|
1646
1416
|
const result = await sendHttp({
|
|
1647
1417
|
auth,
|
|
1648
1418
|
body,
|
|
@@ -1658,9 +1428,9 @@ export class OpenAIOAuthProvider {
|
|
|
1658
1428
|
useModel,
|
|
1659
1429
|
fetchFn: opts._fetchFn,
|
|
1660
1430
|
});
|
|
1661
|
-
|
|
1662
|
-
if (process.env.
|
|
1663
|
-
process.stderr.write(`[
|
|
1431
|
+
if (sticky) markStickyHttpFallback();
|
|
1432
|
+
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
1433
|
+
process.stderr.write(`[agent-trace] provider-send-end elapsed=${Date.now() - _t1}ms result=ok transport=http-fallback\n`);
|
|
1664
1434
|
}
|
|
1665
1435
|
return recordLiveModel(result);
|
|
1666
1436
|
};
|
|
@@ -1681,18 +1451,17 @@ export class OpenAIOAuthProvider {
|
|
|
1681
1451
|
forceFresh,
|
|
1682
1452
|
});
|
|
1683
1453
|
if (opts.forceHttpFallback === true
|
|
1684
|
-
||
|
|
1685
|
-
|| hasImageContent
|
|
1454
|
+
|| httpFallbackActive()
|
|
1686
1455
|
|| _envFlag('MIXDOG_OPENAI_OAUTH_FORCE_HTTP_FALLBACK', false)) {
|
|
1687
|
-
return dispatchHttp(
|
|
1456
|
+
return dispatchHttp('forced');
|
|
1688
1457
|
}
|
|
1689
1458
|
|
|
1690
1459
|
// Prefer WebSocket for hot cache/delta transport; fall back to HTTP/SSE
|
|
1691
1460
|
// after retry-exhausted handshake/acquire/no-first-event failures.
|
|
1692
1461
|
try {
|
|
1693
|
-
if (process.env.
|
|
1462
|
+
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
1463
|
const result = await dispatchWs(false);
|
|
1695
|
-
if (process.env.
|
|
1464
|
+
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] provider-send-end elapsed=${Date.now() - _t1}ms result=ok\n`); }
|
|
1696
1465
|
return recordLiveModel(result);
|
|
1697
1466
|
} catch (err) {
|
|
1698
1467
|
const status = err?.httpStatus;
|
|
@@ -1705,17 +1474,21 @@ export class OpenAIOAuthProvider {
|
|
|
1705
1474
|
const liveTextEmitted = err?.liveTextEmitted === true || err?.unsafeToRetry === true;
|
|
1706
1475
|
if ((status === 401 || status === 403) && !liveTextEmitted) {
|
|
1707
1476
|
process.stderr.write(`[openai-oauth-ws] ${status} — forcing refresh and retrying once over WS\n`);
|
|
1708
|
-
if (process.env.
|
|
1477
|
+
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] provider-${status}-retry attempt=1\n`); }
|
|
1709
1478
|
this._refreshFallbackUntil = 0;
|
|
1710
1479
|
auth = await this.ensureAuth({ forceRefresh: true, reason: String(status) });
|
|
1711
1480
|
try {
|
|
1712
1481
|
const result = await dispatchWs(true);
|
|
1713
|
-
if (process.env.
|
|
1482
|
+
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] provider-send-end elapsed=${Date.now() - _t1}ms result=ok\n`); }
|
|
1714
1483
|
return recordLiveModel(result);
|
|
1715
1484
|
} catch (retryErr) {
|
|
1716
1485
|
if (_shouldUseOpenAIHttpFallback(retryErr, externalSignal)) {
|
|
1717
1486
|
try {
|
|
1718
|
-
return await dispatchHttp(
|
|
1487
|
+
return await dispatchHttp(
|
|
1488
|
+
retryErr?.retryClassifier || retryErr?.code || retryErr?.message || 'ws_auth_retry_failed',
|
|
1489
|
+
retryErr,
|
|
1490
|
+
{ sticky: true },
|
|
1491
|
+
);
|
|
1719
1492
|
} catch (fallbackErr) {
|
|
1720
1493
|
try { retryErr.fallbackError = fallbackErr; } catch {}
|
|
1721
1494
|
throw retryErr;
|
|
@@ -1738,7 +1511,11 @@ export class OpenAIOAuthProvider {
|
|
|
1738
1511
|
}
|
|
1739
1512
|
if (_shouldUseOpenAIHttpFallback(err, externalSignal)) {
|
|
1740
1513
|
try {
|
|
1741
|
-
return await dispatchHttp(
|
|
1514
|
+
return await dispatchHttp(
|
|
1515
|
+
err?.retryClassifier || err?.midstreamClassifier || err?.code || err?.message || 'ws_failed',
|
|
1516
|
+
err,
|
|
1517
|
+
{ sticky: true },
|
|
1518
|
+
);
|
|
1742
1519
|
} catch (fallbackErr) {
|
|
1743
1520
|
try { err.fallbackError = fallbackErr; } catch {}
|
|
1744
1521
|
throw err;
|
|
@@ -1748,7 +1525,7 @@ export class OpenAIOAuthProvider {
|
|
|
1748
1525
|
}
|
|
1749
1526
|
}
|
|
1750
1527
|
async listModels() {
|
|
1751
|
-
// Dynamic lookup via
|
|
1528
|
+
// Dynamic lookup via /backend-api/codex/models. Cached 24h.
|
|
1752
1529
|
// Endpoint returns rich metadata (context_window, reasoning levels,
|
|
1753
1530
|
// visibility) that is more detailed than /v1/models.
|
|
1754
1531
|
const cached = await _loadCodexModelCache();
|
|
@@ -1772,7 +1549,7 @@ export class OpenAIOAuthProvider {
|
|
|
1772
1549
|
},
|
|
1773
1550
|
dispatcher: getLlmDispatcher(),
|
|
1774
1551
|
});
|
|
1775
|
-
if (!res.ok) throw new Error(`
|
|
1552
|
+
if (!res.ok) throw new Error(`openai-oauth list_models ${res.status}`);
|
|
1776
1553
|
const data = await res.json();
|
|
1777
1554
|
const items = Array.isArray(data?.models) ? data.models : [];
|
|
1778
1555
|
const normalized = items.map(m => _normalizeCodexModel(m));
|
|
@@ -1785,7 +1562,7 @@ export class OpenAIOAuthProvider {
|
|
|
1785
1562
|
_lastCodexListModelsError = err?.message || String(err);
|
|
1786
1563
|
process.stderr.write(`[openai-oauth] listModels fetch failed (${_lastCodexListModelsError})\n`);
|
|
1787
1564
|
// No fallback catalog — empty list signals the UI to show a
|
|
1788
|
-
// "catalog unavailable, retry" state.
|
|
1565
|
+
// "catalog unavailable, retry" state. openai-oauth has no equivalent to
|
|
1789
1566
|
// Anthropic's family tokens so there's no meaningful minimal list.
|
|
1790
1567
|
return [];
|
|
1791
1568
|
}
|
|
@@ -1857,7 +1634,64 @@ function _scrubOAuthLoginBody(text) {
|
|
|
1857
1634
|
.replace(/[A-Za-z0-9_-]{32,}\.[A-Za-z0-9._-]+/g, '[REDACTED]');
|
|
1858
1635
|
}
|
|
1859
1636
|
|
|
1860
|
-
|
|
1637
|
+
function _parseOAuthCodeInput(input) {
|
|
1638
|
+
const value = String(input || '').trim();
|
|
1639
|
+
if (!value) return { code: '', state: '' };
|
|
1640
|
+
try {
|
|
1641
|
+
const url = new URL(value);
|
|
1642
|
+
const code = url.searchParams.get('code') || '';
|
|
1643
|
+
const state = url.searchParams.get('state') || '';
|
|
1644
|
+
if (code || state) return { code, state };
|
|
1645
|
+
} catch { /* not a URL */ }
|
|
1646
|
+
if (value.includes('#')) {
|
|
1647
|
+
const [code, state] = value.split('#', 2);
|
|
1648
|
+
return { code: String(code || '').trim(), state: String(state || '').trim() };
|
|
1649
|
+
}
|
|
1650
|
+
if (value.includes('code=')) {
|
|
1651
|
+
const params = new URLSearchParams(value.startsWith('?') ? value.slice(1) : value);
|
|
1652
|
+
return { code: params.get('code') || '', state: params.get('state') || '' };
|
|
1653
|
+
}
|
|
1654
|
+
return { code: value, state: '' };
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
async function exchangeAuthorizationCode({ pkce, code }) {
|
|
1658
|
+
const cleanCode = String(code || '').trim();
|
|
1659
|
+
if (!cleanCode) throw new Error('[openai-oauth] authorization code is required');
|
|
1660
|
+
const tokenRes = await fetch(TOKEN_URL, {
|
|
1661
|
+
method: 'POST',
|
|
1662
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
1663
|
+
body: new URLSearchParams({
|
|
1664
|
+
grant_type: 'authorization_code',
|
|
1665
|
+
code: cleanCode,
|
|
1666
|
+
redirect_uri: REDIRECT_URI,
|
|
1667
|
+
client_id: CLIENT_ID,
|
|
1668
|
+
code_verifier: pkce.verifier,
|
|
1669
|
+
}),
|
|
1670
|
+
redirect: 'error',
|
|
1671
|
+
signal: AbortSignal.timeout(TOKEN_TIMEOUT_MS),
|
|
1672
|
+
});
|
|
1673
|
+
if (!tokenRes.ok) {
|
|
1674
|
+
const text = await tokenRes.text().catch(() => '');
|
|
1675
|
+
throw new Error(`[openai-oauth] token exchange ${tokenRes.status}: ${_scrubOAuthLoginBody(text).slice(0, 500)}`);
|
|
1676
|
+
}
|
|
1677
|
+
const json = await tokenRes.json();
|
|
1678
|
+
if (!json.access_token || !json.refresh_token) {
|
|
1679
|
+
throw new Error('[openai-oauth] token exchange response missing access_token or refresh_token');
|
|
1680
|
+
}
|
|
1681
|
+
const expiresAt = (typeof json.expires_in === 'number'
|
|
1682
|
+
? Date.now() + json.expires_in * 1000
|
|
1683
|
+
: 0) || _expiryFromAccessToken(json.access_token);
|
|
1684
|
+
const tokens = {
|
|
1685
|
+
access_token: json.access_token,
|
|
1686
|
+
refresh_token: json.refresh_token,
|
|
1687
|
+
expires_at: expiresAt,
|
|
1688
|
+
account_id: extractAccountId(json.access_token),
|
|
1689
|
+
};
|
|
1690
|
+
saveTokens(tokens);
|
|
1691
|
+
return tokens;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
export async function beginOAuthLogin() {
|
|
1861
1695
|
const pkce = generatePKCE();
|
|
1862
1696
|
const state = randomBytes(16).toString('hex');
|
|
1863
1697
|
const url = new URL(AUTHORIZE_URL);
|
|
@@ -1872,18 +1706,20 @@ export async function loginOAuth() {
|
|
|
1872
1706
|
url.searchParams.set('state', state);
|
|
1873
1707
|
url.searchParams.set('originator', CODEX_OAUTH_ORIGINATOR);
|
|
1874
1708
|
|
|
1875
|
-
|
|
1709
|
+
let server = null;
|
|
1710
|
+
let timeout = null;
|
|
1711
|
+
let finish = null;
|
|
1712
|
+
const waitForCallback = new Promise((resolve, reject) => {
|
|
1876
1713
|
let settled = false;
|
|
1877
|
-
|
|
1878
|
-
const settle = (value, error = null) => {
|
|
1714
|
+
finish = (value, error = null) => {
|
|
1879
1715
|
if (settled) return;
|
|
1880
1716
|
settled = true;
|
|
1881
1717
|
if (timeout) clearTimeout(timeout);
|
|
1882
|
-
try { server
|
|
1718
|
+
try { server?.close(); } catch { /* already closed */ }
|
|
1883
1719
|
if (error) reject(error);
|
|
1884
1720
|
else resolve(value);
|
|
1885
1721
|
};
|
|
1886
|
-
|
|
1722
|
+
server = createServer(async (req, res) => {
|
|
1887
1723
|
const u = new URL(req.url || '/', `http://${CALLBACK_HOST}:${CALLBACK_PORT}`);
|
|
1888
1724
|
if (u.pathname !== CALLBACK_PATH) {
|
|
1889
1725
|
res.writeHead(404);
|
|
@@ -1894,53 +1730,21 @@ export async function loginOAuth() {
|
|
|
1894
1730
|
if (!code || u.searchParams.get('state') !== state) {
|
|
1895
1731
|
res.writeHead(400);
|
|
1896
1732
|
res.end('Invalid');
|
|
1897
|
-
|
|
1733
|
+
finish(null);
|
|
1898
1734
|
return;
|
|
1899
1735
|
}
|
|
1900
1736
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1901
|
-
res.end('<html><body><h2>
|
|
1737
|
+
res.end('<html><body><h2>OpenAI OAuth login successful! You can close this tab.</h2></body></html>');
|
|
1902
1738
|
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);
|
|
1739
|
+
const tokens = await exchangeAuthorizationCode({ pkce, code });
|
|
1740
|
+
finish(tokens);
|
|
1937
1741
|
} catch (err) {
|
|
1938
|
-
|
|
1742
|
+
finish(null, err instanceof Error ? err : new Error(String(err)));
|
|
1939
1743
|
}
|
|
1940
1744
|
});
|
|
1941
|
-
timeout = setTimeout(() =>
|
|
1745
|
+
timeout = setTimeout(() => finish(null), LOGIN_TIMEOUT_MS);
|
|
1942
1746
|
server.listen(CALLBACK_PORT, CALLBACK_HOST, async () => {
|
|
1943
|
-
process.stderr.write(`\n[openai-oauth] Open this URL to log in to ChatGPT (
|
|
1747
|
+
process.stderr.write(`\n[openai-oauth] Open this URL to log in to ChatGPT (OpenAI OAuth):\n${url.toString()}\n\n`);
|
|
1944
1748
|
try {
|
|
1945
1749
|
const { openInBrowser } = await import('../../../shared/open-url.mjs');
|
|
1946
1750
|
openInBrowser(url.toString());
|
|
@@ -1948,6 +1752,27 @@ export async function loginOAuth() {
|
|
|
1948
1752
|
process.stderr.write(`[openai-oauth] browser open failed: ${String(err?.message || err).slice(0, 200)}\n`);
|
|
1949
1753
|
}
|
|
1950
1754
|
});
|
|
1951
|
-
server.on('error', (err) =>
|
|
1755
|
+
server.on('error', (err) => finish(null, new Error(`[openai-oauth] callback server failed on ${CALLBACK_HOST}:${CALLBACK_PORT}: ${err?.message || err}`)));
|
|
1952
1756
|
});
|
|
1757
|
+
|
|
1758
|
+
return {
|
|
1759
|
+
provider: 'openai-oauth',
|
|
1760
|
+
url: url.toString(),
|
|
1761
|
+
waitForCallback,
|
|
1762
|
+
completeCode: async (input) => {
|
|
1763
|
+
const parsed = _parseOAuthCodeInput(input);
|
|
1764
|
+
if (parsed.state && parsed.state !== state) throw new Error('[openai-oauth] OAuth state mismatch');
|
|
1765
|
+
const tokens = await exchangeAuthorizationCode({ pkce, code: parsed.code });
|
|
1766
|
+
finish?.(tokens);
|
|
1767
|
+
return tokens;
|
|
1768
|
+
},
|
|
1769
|
+
cancel: () => {
|
|
1770
|
+
finish?.(null);
|
|
1771
|
+
},
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
export async function loginOAuth() {
|
|
1776
|
+
const login = await beginOAuthLogin();
|
|
1777
|
+
return await login.waitForCallback;
|
|
1953
1778
|
}
|