mixdog 0.9.3 → 0.9.4
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/package.json +7 -3
- package/scripts/bench/lead-review-tasks-r3.json +20 -0
- package/scripts/bench/lead-review-tasks.json +20 -0
- package/scripts/bench/r4-mixed-tasks.json +20 -0
- package/scripts/bench/review-tasks.json +20 -0
- package/scripts/bench/round-codex.json +114 -0
- package/scripts/bench/round-mixdog-lead-r3.json +269 -0
- package/scripts/bench/round-mixdog-lead.json +269 -0
- package/scripts/bench/round-mixdog.json +126 -0
- package/scripts/bench/round-r10-bigsample.json +679 -0
- package/scripts/bench/round-r11-codexalign.json +257 -0
- package/scripts/bench/round-r4-codex.json +114 -0
- package/scripts/bench/round-r4-mixed.json +225 -0
- package/scripts/bench/round-r5-gpt-lead.json +259 -0
- package/scripts/bench/round-r6-codex.json +114 -0
- package/scripts/bench/round-r6-solo.json +257 -0
- package/scripts/bench/round-r7-full.json +254 -0
- package/scripts/bench/round-r8-fulldefault.json +255 -0
- package/scripts/bench-run.mjs +215 -29
- package/scripts/freevar-smoke.mjs +95 -0
- package/scripts/internal-comms-bench.mjs +1 -0
- package/scripts/internal-comms-smoke.mjs +10 -9
- package/scripts/mouse-probe.mjs +45 -0
- package/scripts/output-style-bench.mjs +13 -6
- package/scripts/output-style-smoke.mjs +4 -4
- package/scripts/provider-toolcall-test.mjs +7 -3
- package/scripts/recall-usecase-cases.json +18 -0
- package/scripts/recall-usecase-probe.json +6 -0
- package/scripts/session-bench.mjs +152 -6
- package/scripts/tool-smoke.mjs +23 -63
- package/scripts/tui-render-smoke.mjs +90 -0
- package/scripts/webhook-smoke.mjs +208 -0
- package/src/agents/debugger/AGENT.md +4 -1
- package/src/agents/heavy-worker/AGENT.md +6 -5
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +2 -1
- package/src/agents/worker/AGENT.md +8 -4
- package/src/lib/rules-builder.cjs +4 -0
- package/src/mixdog-session-runtime.mjs +632 -2042
- package/src/output-styles/default.md +34 -9
- package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
- package/src/output-styles/minimal.md +4 -1
- package/src/output-styles/simple.md +22 -7
- package/src/rules/agent/00-common.md +2 -0
- package/src/rules/lead/lead-brief.md +12 -0
- package/src/rules/lead/lead-tool.md +0 -11
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
- package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
- package/src/runtime/agent/orchestrator/config.mjs +69 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +86 -11
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +348 -0
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +732 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +297 -2123
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +130 -1002
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +227 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +436 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
- package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
- package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
- package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
- package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +182 -0
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +251 -397
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +32 -41
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
- package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
- package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
- package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
- package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
- package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
- package/src/runtime/channels/backends/discord-access.mjs +32 -0
- package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
- package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
- package/src/runtime/channels/backends/discord.mjs +12 -292
- package/src/runtime/channels/index.mjs +229 -663
- package/src/runtime/channels/lib/backend-dispatch.mjs +44 -0
- package/src/runtime/channels/lib/event-pipeline.mjs +18 -1
- package/src/runtime/channels/lib/event-queue.mjs +63 -4
- package/src/runtime/channels/lib/inbound-routing.mjs +111 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
- package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
- package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
- package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
- package/src/runtime/channels/lib/session-discovery.mjs +56 -4
- package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
- package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
- package/src/runtime/channels/lib/webhook/deliveries.mjs +312 -0
- package/src/runtime/channels/lib/webhook/log.mjs +42 -0
- package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
- package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
- package/src/runtime/channels/lib/webhook.mjs +36 -570
- package/src/runtime/channels/tool-defs.mjs +11 -130
- package/src/runtime/memory/index.mjs +201 -1948
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
- package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
- package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
- package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
- package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
- package/src/runtime/memory/lib/memory-embed.mjs +149 -0
- package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +22 -2
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
- package/src/runtime/memory/lib/query-handlers.mjs +780 -0
- package/src/runtime/memory/lib/recall-format.mjs +55 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
- package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
- package/src/runtime/memory/tool-defs.mjs +5 -13
- package/src/runtime/search/lib/http-fetch.mjs +274 -0
- package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
- package/src/runtime/search/lib/web-tools.mjs +24 -602
- package/src/runtime/shared/atomic-file.mjs +26 -1
- package/src/runtime/shared/launcher-control.mjs +2 -2
- package/src/runtime/shared/tool-primitives.mjs +308 -0
- package/src/runtime/shared/tool-result-summary.mjs +515 -0
- package/src/runtime/shared/tool-surface.mjs +80 -898
- package/src/runtime/shared/transcript-writer.mjs +23 -0
- package/src/runtime/shared/update-checker.mjs +7 -4
- package/src/session-runtime/config-helpers.mjs +84 -2
- package/src/session-runtime/config-lifecycle.mjs +232 -0
- package/src/session-runtime/cwd-plugins.mjs +226 -0
- package/src/session-runtime/mcp-glue.mjs +177 -0
- package/src/session-runtime/model-recency.mjs +111 -0
- package/src/session-runtime/native-search.mjs +247 -0
- package/src/session-runtime/output-styles.mjs +11 -9
- package/src/session-runtime/prewarm.mjs +142 -0
- package/src/session-runtime/provider-models.mjs +278 -0
- package/src/session-runtime/provider-usage.mjs +120 -0
- package/src/session-runtime/quick-model-rows.mjs +170 -0
- package/src/session-runtime/quick-search-models.mjs +46 -0
- package/src/session-runtime/session-hooks.mjs +93 -0
- package/src/session-runtime/settings-api.mjs +319 -0
- package/src/session-runtime/tool-catalog.mjs +29 -29
- package/src/session-runtime/tool-defs.mjs +84 -0
- package/src/session-runtime/warmup-schedulers.mjs +201 -0
- package/src/standalone/agent-tool/helpers.mjs +237 -0
- package/src/standalone/agent-tool/notify.mjs +107 -0
- package/src/standalone/agent-tool/provider-init.mjs +143 -0
- package/src/standalone/agent-tool/render.mjs +152 -0
- package/src/standalone/agent-tool/tool-def.mjs +55 -0
- package/src/standalone/agent-tool.mjs +110 -671
- package/src/standalone/channel-worker.mjs +4 -7
- package/src/standalone/explore-tool.mjs +30 -9
- package/src/standalone/hook-bus/config.mjs +207 -0
- package/src/standalone/hook-bus/constants.mjs +90 -0
- package/src/standalone/hook-bus/handlers.mjs +481 -0
- package/src/standalone/hook-bus/payload.mjs +31 -0
- package/src/standalone/hook-bus/rules.mjs +77 -0
- package/src/standalone/hook-bus.mjs +77 -870
- package/src/standalone/memory-runtime-proxy.mjs +7 -0
- package/src/standalone/opencode-go-login.mjs +5 -1
- package/src/standalone/provider-admin.mjs +1 -16
- package/src/standalone/usage-dashboard.mjs +3 -1
- package/src/tui/App.jsx +945 -8094
- package/src/tui/app/app-format.mjs +206 -0
- package/src/tui/app/channel-pickers.mjs +510 -0
- package/src/tui/app/clipboard.mjs +67 -0
- package/src/tui/app/core-memory-picker.mjs +210 -0
- package/src/tui/app/extension-pickers.mjs +506 -0
- package/src/tui/app/input-parsers.mjs +193 -0
- package/src/tui/app/maintenance-pickers.mjs +324 -0
- package/src/tui/app/model-options.mjs +330 -0
- package/src/tui/app/model-picker.mjs +365 -0
- package/src/tui/app/onboarding-steps.mjs +400 -0
- package/src/tui/app/project-picker.mjs +247 -0
- package/src/tui/app/provider-setup-picker.mjs +580 -0
- package/src/tui/app/resume-picker.mjs +55 -0
- package/src/tui/app/route-pickers.mjs +419 -0
- package/src/tui/app/settings-picker.mjs +490 -0
- package/src/tui/app/slash-commands.mjs +101 -0
- package/src/tui/app/slash-dispatch.mjs +427 -0
- package/src/tui/app/text-layout.mjs +46 -0
- package/src/tui/app/theme-effort-pickers.mjs +154 -0
- package/src/tui/app/transcript-window.mjs +671 -0
- package/src/tui/app/use-mouse-input.mjs +460 -0
- package/src/tui/app/use-prompt-handlers.mjs +310 -0
- package/src/tui/app/use-transcript-scroll.mjs +510 -0
- package/src/tui/app/use-transcript-window.mjs +589 -0
- package/src/tui/components/ConfirmBar.jsx +1 -1
- package/src/tui/components/Picker.jsx +32 -4
- package/src/tui/components/PromptInput.jsx +23 -101
- package/src/tui/components/SlashCommandPalette.jsx +8 -1
- package/src/tui/components/StatusLine.jsx +63 -12
- package/src/tui/components/TextEntryPanel.jsx +11 -0
- package/src/tui/components/ToolExecution.jsx +52 -594
- package/src/tui/components/TranscriptItem.jsx +105 -0
- package/src/tui/components/UsagePanel.jsx +18 -4
- package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
- package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
- package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
- package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
- package/src/tui/components/tool-execution/text-format.mjs +161 -0
- package/src/tui/display-width.mjs +20 -3
- package/src/tui/dist/index.mjs +19652 -18630
- package/src/tui/engine/agent-job-feed.mjs +133 -0
- package/src/tui/engine/notification-plan.mjs +76 -0
- package/src/tui/engine/render-timing.mjs +17 -0
- package/src/tui/engine/tool-approval.mjs +94 -0
- package/src/tui/engine/tool-card-results.mjs +234 -0
- package/src/tui/engine/tool-result-status.mjs +135 -0
- package/src/tui/engine.mjs +122 -562
- package/src/tui/figures.mjs +5 -0
- package/src/tui/index.jsx +105 -0
- package/src/tui/input-editing.mjs +2 -2
- package/src/tui/markdown/format-token.mjs +4 -1
- package/src/tui/statusline-ansi-bridge.mjs +11 -3
- package/src/tui/theme.mjs +6 -0
- package/src/ui/statusline-agents.mjs +213 -0
- package/src/ui/statusline-format.mjs +146 -0
- package/src/ui/statusline-segments.mjs +148 -0
- package/src/ui/statusline.mjs +67 -501
- package/src/ui/tool-card.mjs +0 -1
- package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
- package/src/workflows/default/WORKFLOW.md +1 -1
- package/src/workflows/sequential/WORKFLOW.md +1 -1
- package/vendor/ink/build/display-width.js +19 -3
- package/vendor/ink/build/ink.js +103 -6
- package/vendor/ink/build/log-update.js +17 -3
- package/vendor/ink/build/wrap-text.js +125 -0
- package/scripts/_test-folder-dialog.mjs +0 -30
- package/scripts/fix-brief-fn.mjs +0 -35
- package/scripts/fix-format-tool-surface.mjs +0 -24
- package/scripts/fix-tool-exec-visible.mjs +0 -42
- package/scripts/patch-agent-brief.mjs +0 -48
- package/scripts/patch-app.mjs +0 -21
- package/scripts/patch-app2.mjs +0 -18
- package/scripts/patch-dist-brief.mjs +0 -96
- package/scripts/patch-tool-exec.mjs +0 -70
- package/src/examples/schedules/SCHEDULE.example.md +0 -32
- package/src/examples/webhooks/WEBHOOK.example.md +0 -40
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
- package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
- package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
- package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
- package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
- package/src/standalone/agent-task-status.test.mjs +0 -76
- package/src/tui/components/tool-output-format.test.mjs +0 -399
- package/src/tui/display-width.test.mjs +0 -35
- package/src/tui/engine-runtime-notification.test.mjs +0 -115
- package/src/tui/engine-tool-result-text.test.mjs +0 -75
- package/src/tui/input-editing.selection.test.mjs +0 -75
- package/src/tui/markdown/format-token.test.mjs +0 -354
- package/src/tui/markdown/render-ansi.test.mjs +0 -108
- package/src/tui/markdown/stream-fence.test.mjs +0 -26
- package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
- package/src/tui/paste-fix.test.mjs +0 -119
- package/src/tui/prompt-history-store.test.mjs +0 -52
- package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
- package/src/tui/transcript-tool-failures.test.mjs +0 -111
- package/src/ui/markdown.test.mjs +0 -70
- package/src/ui/statusline-context-label.test.mjs +0 -15
- package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
- package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
|
@@ -7,12 +7,12 @@
|
|
|
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
|
*/
|
|
10
|
-
import {
|
|
11
|
-
import { randomBytes, createHash } from 'crypto';
|
|
10
|
+
import { createHash } from 'crypto';
|
|
12
11
|
import { readFileSync, existsSync, mkdirSync, statSync, unlinkSync } from 'fs';
|
|
13
12
|
import { join } from 'path';
|
|
14
13
|
import { getPluginData } from '../config.mjs';
|
|
15
14
|
import { enrichModels } from './model-catalog.mjs';
|
|
15
|
+
import { sanitizeModelList } from './model-list-sanitize.mjs';
|
|
16
16
|
import { writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
|
|
17
17
|
import { makeModelCache } from './model-cache.mjs';
|
|
18
18
|
|
|
@@ -51,11 +51,31 @@ import {
|
|
|
51
51
|
isResponsesFreeformTool,
|
|
52
52
|
toResponsesCustomTool,
|
|
53
53
|
} from './custom-tool-wire.mjs';
|
|
54
|
+
import {
|
|
55
|
+
sendViaHttpSse,
|
|
56
|
+
_envFlag,
|
|
57
|
+
_envPositiveInt,
|
|
58
|
+
_shouldUseOpenAIHttpFallback,
|
|
59
|
+
} from './openai-oauth-http-sse.mjs';
|
|
60
|
+
import { createOpenAIOAuthLogin } from './openai-oauth-login.mjs';
|
|
61
|
+
import {
|
|
62
|
+
_displayCodexModel,
|
|
63
|
+
_codexFamily,
|
|
64
|
+
_normalizeCodexModel,
|
|
65
|
+
_compareVersion,
|
|
66
|
+
_isMainCodexFamily,
|
|
67
|
+
_markLatestCodex,
|
|
68
|
+
} from './openai-codex-model.mjs';
|
|
69
|
+
export { _displayCodexModel };
|
|
70
|
+
|
|
71
|
+
// Legacy import path for scripts/tool-smoke.mjs (single-emit SSE smoke).
|
|
72
|
+
export { sendViaHttpSse };
|
|
54
73
|
// --- Constants ---
|
|
55
74
|
const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
56
|
-
|
|
75
|
+
// Exported for openai-oauth-http-sse.mjs (fallback transport headers/URL).
|
|
76
|
+
export const CODEX_OAUTH_ORIGINATOR = 'codex_cli_rs';
|
|
57
77
|
const TOKEN_URL = 'https://auth.openai.com/oauth/token';
|
|
58
|
-
const CODEX_RESPONSES_URL = 'https://chatgpt.com/backend-api/codex/responses';
|
|
78
|
+
export const CODEX_RESPONSES_URL = 'https://chatgpt.com/backend-api/codex/responses';
|
|
59
79
|
// Version string baked into the models endpoint query — the OAuth backend
|
|
60
80
|
// rejects the request without it, and gates new model exposures (e.g.
|
|
61
81
|
// gpt-5.5 only on >= 0.130.0) on this client_version header; older versions
|
|
@@ -63,7 +83,11 @@ const CODEX_RESPONSES_URL = 'https://chatgpt.com/backend-api/codex/responses';
|
|
|
63
83
|
// dynamically from npm so newly-shipped models surface within a day instead
|
|
64
84
|
// of waiting on a hardcoded bump here. Cached 24h in-process; npm failure
|
|
65
85
|
// falls back to the floor below.
|
|
66
|
-
|
|
86
|
+
// Offline fallback only — _resolveCodexClientVersion() fetches the live
|
|
87
|
+
// @openai/codex latest from npm first. Bumped to the current release
|
|
88
|
+
// (0.142.5, verified 2026-07-03) so the offline path stays close to what the
|
|
89
|
+
// backend expects for client-version gating.
|
|
90
|
+
const CODEX_CLIENT_VERSION_FLOOR = '0.142.5';
|
|
67
91
|
const CODEX_VERSION_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
68
92
|
let _codexVersionCache = { value: null, fetchedAt: 0 };
|
|
69
93
|
|
|
@@ -118,10 +142,6 @@ let _codexRefreshInFlight = null;
|
|
|
118
142
|
let _oauthRefreshInFlight = null;
|
|
119
143
|
let _lastCodexListModelsError = '';
|
|
120
144
|
|
|
121
|
-
export function getOpenAIOAuthModelCatalogError() {
|
|
122
|
-
return _lastCodexListModelsError;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
145
|
function _codexCatalogHas(id) {
|
|
126
146
|
if (!id || !Array.isArray(_inMemoryCodexCatalog)) return false;
|
|
127
147
|
return _inMemoryCodexCatalog.some(m => m.id === id);
|
|
@@ -155,106 +175,6 @@ export function codexModelSupportsServiceTier(id, serviceTier) {
|
|
|
155
175
|
return tiers.some(t => t?.id === serviceTier);
|
|
156
176
|
}
|
|
157
177
|
|
|
158
|
-
// OAuth catalog returns dated ids (gpt-5.4-mini-2026-03-17). Strip the trailing
|
|
159
|
-
// -YYYY-MM-DD to get the version alias (gpt-5.4-mini). Unknown shapes pass
|
|
160
|
-
// through unchanged.
|
|
161
|
-
function _displayCodexModel(id) {
|
|
162
|
-
if (!id || typeof id !== 'string') return id;
|
|
163
|
-
return id.replace(/-\d{4}-\d{2}-\d{2}$/, '');
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function _positiveCodexContextWindow(value) {
|
|
167
|
-
const n = Number(value);
|
|
168
|
-
return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function _codexContextWindowFromApi(m) {
|
|
172
|
-
return _positiveCodexContextWindow(m?.context_window)
|
|
173
|
-
|| _positiveCodexContextWindow(m?.max_context_window)
|
|
174
|
-
|| null;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function _normalizeCodexModel(m) {
|
|
178
|
-
const id = m?.slug || m?.id;
|
|
179
|
-
const family = _codexFamily(id);
|
|
180
|
-
const serviceTiers = Array.isArray(m?.service_tiers)
|
|
181
|
-
? m.service_tiers
|
|
182
|
-
.map(t => ({
|
|
183
|
-
id: String(t?.id || '').trim(),
|
|
184
|
-
name: String(t?.name || '').trim(),
|
|
185
|
-
description: String(t?.description || '').trim(),
|
|
186
|
-
}))
|
|
187
|
-
.filter(t => t.id)
|
|
188
|
-
: [];
|
|
189
|
-
const additionalSpeedTiers = Array.isArray(m?.additional_speed_tiers)
|
|
190
|
-
? m.additional_speed_tiers.map(t => String(t || '').trim()).filter(Boolean)
|
|
191
|
-
: [];
|
|
192
|
-
// Catalog ids are version aliases without separate display dating.
|
|
193
|
-
return {
|
|
194
|
-
id,
|
|
195
|
-
name: m?.display_name || id,
|
|
196
|
-
display: m?.display_name || id,
|
|
197
|
-
family,
|
|
198
|
-
provider: 'openai-oauth',
|
|
199
|
-
contextWindow: _codexContextWindowFromApi(m),
|
|
200
|
-
maxContextWindow: _positiveCodexContextWindow(m?.max_context_window),
|
|
201
|
-
outputTokens: m?.max_output_tokens || m?.output_tokens || 32768,
|
|
202
|
-
autoCompactTokenLimit: m?.auto_compact_token_limit || null,
|
|
203
|
-
effectiveContextWindowPercent: m?.effective_context_window_percent || null,
|
|
204
|
-
tier: 'version',
|
|
205
|
-
latest: false,
|
|
206
|
-
description: m?.description || '',
|
|
207
|
-
reasoningLevels: (m?.supported_reasoning_levels || []).map(r => r.effort),
|
|
208
|
-
serviceTiers,
|
|
209
|
-
defaultServiceTier: m?.default_service_tier || null,
|
|
210
|
-
additionalSpeedTiers,
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function _codexFamily(id) {
|
|
215
|
-
const s = String(id || '').toLowerCase();
|
|
216
|
-
if (s.includes('nano')) return 'gpt-nano';
|
|
217
|
-
if (s.includes('mini')) return 'gpt-mini';
|
|
218
|
-
if (s.includes('codex')) return 'gpt-codex';
|
|
219
|
-
if (s.startsWith('gpt-5.5')) return 'gpt-5.5';
|
|
220
|
-
if (s.startsWith('gpt-5.4')) return 'gpt-5.4';
|
|
221
|
-
if (s.startsWith('gpt-5.2')) return 'gpt-5.2';
|
|
222
|
-
if (s.startsWith('gpt-5')) return 'gpt-5';
|
|
223
|
-
return 'gpt';
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Compare two model ids by the X.Y version embedded in `gpt-X.Y`. Mirrors
|
|
227
|
-
// anthropic-oauth's _compareVersion; these ids have no trailing date so
|
|
228
|
-
// the version lives in the dotted number, not a -YYYY-MM-DD suffix.
|
|
229
|
-
function _compareVersion(a, b) {
|
|
230
|
-
const na = (String(a).match(/gpt-(\d+)\.(\d+)/) || []).slice(1).map(Number);
|
|
231
|
-
const nb = (String(b).match(/gpt-(\d+)\.(\d+)/) || []).slice(1).map(Number);
|
|
232
|
-
for (let i = 0; i < Math.max(na.length, nb.length); i++) {
|
|
233
|
-
if ((na[i] || 0) !== (nb[i] || 0)) return (na[i] || 0) - (nb[i] || 0);
|
|
234
|
-
}
|
|
235
|
-
return String(a).localeCompare(String(b));
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Main gpt-5 chat family only: exclude the mini/nano/codex variants so "latest"
|
|
239
|
-
// resolves to the flagship, not a smaller sibling.
|
|
240
|
-
function _isMainCodexFamily(family) {
|
|
241
|
-
return typeof family === 'string' && family.startsWith('gpt-5');
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Mark the highest-version model per family as `latest: true`. VERSION-based
|
|
245
|
-
// (ids carry no `created`), mirroring anthropic-oauth's per-family pass.
|
|
246
|
-
function _markLatestCodex(models) {
|
|
247
|
-
const byFamily = new Map();
|
|
248
|
-
for (const m of models) {
|
|
249
|
-
if (!m?.id) continue;
|
|
250
|
-
const cur = byFamily.get(m.family);
|
|
251
|
-
if (!cur || _compareVersion(m.id, cur.id) > 0) {
|
|
252
|
-
byFamily.set(m.family, m);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
for (const m of byFamily.values()) m.latest = true;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
178
|
// Newest MAIN gpt-5 chat model by version, read from the SYNC in-memory
|
|
259
179
|
// catalog mirror. Returns null until populated; callers warm via
|
|
260
180
|
// ensureLatestCodexModel when null.
|
|
@@ -577,7 +497,35 @@ function toOpenAIResponsesTool(t) {
|
|
|
577
497
|
};
|
|
578
498
|
}
|
|
579
499
|
|
|
500
|
+
// codex build_reasoning() (core/src/client.rs:785-805) only attaches the
|
|
501
|
+
// reasoning object when model_info.supports_reasoning_summaries; models
|
|
502
|
+
// without summary support get NO reasoning field at all. Mirror that via the
|
|
503
|
+
// cached codex catalog; unknown models default to true (gpt-5 family all
|
|
504
|
+
// support summaries) so a cold catalog cannot strip reasoning from the wire.
|
|
505
|
+
function _codexModelSupportsReasoningSummaries(id) {
|
|
506
|
+
const info = _findCachedCodexModel(id);
|
|
507
|
+
if (!info) return true;
|
|
508
|
+
const flags = [info.supportsReasoningSummaries, info.supports_reasoning_summaries, info.supportsReasoning, info.supports_reasoning];
|
|
509
|
+
for (const flag of flags) {
|
|
510
|
+
if (typeof flag === 'boolean') return flag;
|
|
511
|
+
}
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// codex reasoning_effort_for_request (core/src/client.rs): `ultra` collapses to
|
|
516
|
+
// `max` on the wire — the openai-oauth backend does not accept `ultra`. Every
|
|
517
|
+
// other effort passes through unchanged; empty/unknown falls back to medium.
|
|
518
|
+
function _normalizeReasoningEffort(effort) {
|
|
519
|
+
const e = String(effort || '').trim().toLowerCase();
|
|
520
|
+
if (!e) return 'medium';
|
|
521
|
+
if (e === 'ultra') return 'max';
|
|
522
|
+
return e;
|
|
523
|
+
}
|
|
524
|
+
|
|
580
525
|
export function buildRequestBody(messages, model, tools, sendOpts) {
|
|
526
|
+
// codex reasoning_effort_for_request: `ultra` collapses to `max` on the
|
|
527
|
+
// wire (the only remap; every other effort passes through). Default medium.
|
|
528
|
+
// Kept inline (not a module const) so buildRequestBody stays self-contained.
|
|
581
529
|
// Extract system/instructions
|
|
582
530
|
const systemMsgs = messages.filter(m => m.role === 'system');
|
|
583
531
|
const instructions = systemMsgs.map(m => m.content).join('\n\n') || 'You are a helpful assistant.';
|
|
@@ -596,17 +544,32 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
596
544
|
const value = String(item || '').trim();
|
|
597
545
|
if (value && !include.includes(value)) include.push(value);
|
|
598
546
|
}
|
|
547
|
+
// Field order MIRRORS codex-rs ResponsesApiRequest (common.rs struct order):
|
|
548
|
+
// model, instructions, input, tools, tool_choice, parallel_tool_calls,
|
|
549
|
+
// reasoning, store, stream, include, service_tier, prompt_cache_key, text.
|
|
550
|
+
// JSON serialization order is load-bearing for the server prompt cache
|
|
551
|
+
// (exact-prefix match): matching codex's byte layout keeps our requests on
|
|
552
|
+
// the same cache-routing shape codex warms. tools/service_tier/
|
|
553
|
+
// prompt_cache_key are appended below in the same relative order.
|
|
599
554
|
const body = {
|
|
600
555
|
model,
|
|
601
556
|
instructions,
|
|
602
557
|
input,
|
|
558
|
+
tool_choice: opts.toolChoice || 'auto',
|
|
559
|
+
parallel_tool_calls: true,
|
|
560
|
+
// codex build_reasoning() sends { effort, summary } — summary defaults to
|
|
561
|
+
// ReasoningSummary::Auto (protocol config_types.rs), serialized lowercase
|
|
562
|
+
// as "auto". Matching this keeps our reasoning object byte-identical to
|
|
563
|
+
// codex so the server prompt-cache prefix hash lines up. codex also
|
|
564
|
+
// normalizes `ultra` -> `max` on the wire (reasoning_effort_for_request
|
|
565
|
+
// in core/src/client.rs); the openai-oauth backend does not accept
|
|
566
|
+
// `ultra` as a wire value, so mirror that mapping here.
|
|
567
|
+
...(_codexModelSupportsReasoningSummaries(model)
|
|
568
|
+
? { reasoning: { effort: _normalizeReasoningEffort(opts.effort), summary: 'auto' } }
|
|
569
|
+
: {}),
|
|
603
570
|
store: process.env.MIXDOG_OAI_STORE === 'true' ? true : false,
|
|
604
571
|
stream: true,
|
|
605
|
-
reasoning: { effort: opts.effort || 'medium' },
|
|
606
|
-
text: { verbosity: 'medium' },
|
|
607
572
|
include,
|
|
608
|
-
tool_choice: opts.toolChoice || 'auto',
|
|
609
|
-
parallel_tool_calls: true,
|
|
610
573
|
};
|
|
611
574
|
const maxOutputTokens = Number(opts.maxOutputTokens ?? opts.outputTokens ?? opts.max_output_tokens);
|
|
612
575
|
if (_envFlag('MIXDOG_OPENAI_OAUTH_SEND_MAX_OUTPUT_TOKENS', false)
|
|
@@ -625,20 +588,22 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
625
588
|
}
|
|
626
589
|
// Add tools. `nativeTools` are server-hosted Responses tools (for
|
|
627
590
|
// example web_search) and must be passed through without wrapping them as
|
|
628
|
-
// function tools.
|
|
591
|
+
// function tools. codex places `tools` right after `input` (before
|
|
592
|
+
// tool_choice); we insert it there via a rebuilt object so serialization
|
|
593
|
+
// order matches, rather than appending it last.
|
|
629
594
|
const functionTools = tools?.length ? tools.map(toOpenAIResponsesTool) : [];
|
|
630
595
|
const nativeTools = Array.isArray(opts.nativeTools)
|
|
631
596
|
? opts.nativeTools.filter(t => t && typeof t === 'object')
|
|
632
597
|
: [];
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
598
|
+
const toolsList = (functionTools.length || nativeTools.length)
|
|
599
|
+
? [...nativeTools, ...functionTools]
|
|
600
|
+
: null;
|
|
636
601
|
const promptCacheProvider = opts.promptCacheProvider || 'openai-oauth';
|
|
637
602
|
const promptCacheLane = opts.promptCacheLane || resolveProviderPromptCacheLane(promptCacheProvider, opts);
|
|
638
|
-
|
|
603
|
+
const promptCacheKey = buildStableProviderPromptCacheKey(promptCacheProvider, opts, {
|
|
639
604
|
model,
|
|
640
605
|
instructions,
|
|
641
|
-
tools:
|
|
606
|
+
tools: toolsList || [],
|
|
642
607
|
effort: body.reasoning?.effort,
|
|
643
608
|
fast: opts.fast === true,
|
|
644
609
|
serviceTier: body.service_tier || '',
|
|
@@ -647,716 +612,41 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
647
612
|
cacheLaneSlot: promptCacheLane.slot,
|
|
648
613
|
cacheLaneShards: promptCacheLane.shards,
|
|
649
614
|
});
|
|
615
|
+
// codex only serializes `text` when a verbosity/schema is configured
|
|
616
|
+
// (common.rs create_text_param_for_request returns None otherwise); it has
|
|
617
|
+
// no unconditional { verbosity: 'medium' }. Sending an extra text object
|
|
618
|
+
// shifts every following byte vs codex's prefix layout.
|
|
619
|
+
const verbosity = typeof opts.verbosity === 'string' && opts.verbosity.trim()
|
|
620
|
+
? opts.verbosity.trim().toLowerCase()
|
|
621
|
+
: null;
|
|
622
|
+
// Rebuild the body in codex struct order so JSON serialization is
|
|
623
|
+
// byte-compatible with codex: ... input, tools, tool_choice,
|
|
624
|
+
// parallel_tool_calls, reasoning, store, stream, include, service_tier,
|
|
625
|
+
// prompt_cache_key, text. service_tier is only present when fast set it.
|
|
626
|
+
const ordered = {
|
|
627
|
+
model: body.model,
|
|
628
|
+
instructions: body.instructions,
|
|
629
|
+
input: body.input,
|
|
630
|
+
...(toolsList ? { tools: toolsList } : {}),
|
|
631
|
+
tool_choice: body.tool_choice,
|
|
632
|
+
parallel_tool_calls: body.parallel_tool_calls,
|
|
633
|
+
reasoning: body.reasoning,
|
|
634
|
+
store: body.store,
|
|
635
|
+
stream: body.stream,
|
|
636
|
+
include: body.include,
|
|
637
|
+
...(body.service_tier ? { service_tier: body.service_tier } : {}),
|
|
638
|
+
prompt_cache_key: promptCacheKey,
|
|
639
|
+
...(verbosity ? { text: { verbosity } } : {}),
|
|
640
|
+
...(body.max_output_tokens ? { max_output_tokens: body.max_output_tokens } : {}),
|
|
641
|
+
};
|
|
650
642
|
// NOTE: prompt_cache_retention is a public OpenAI Responses API parameter,
|
|
651
643
|
// but the openai-oauth endpoint still rejects it ("Unsupported parameter:
|
|
652
644
|
// prompt_cache_retention", re-probed 2026-06-22). Leave retention on the
|
|
653
645
|
// openai-oauth server default; public OpenAI direct injects 24h separately.
|
|
654
|
-
return
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
function _envFlag(name, fallback = true) {
|
|
658
|
-
const raw = process.env[name];
|
|
659
|
-
if (raw == null || raw === '') return fallback;
|
|
660
|
-
return !['0', 'false', 'off', 'no'].includes(String(raw).toLowerCase());
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
function _envPositiveInt(name, fallback) {
|
|
664
|
-
const raw = process.env[name];
|
|
665
|
-
if (raw == null || raw === '') return fallback;
|
|
666
|
-
const n = Number(raw);
|
|
667
|
-
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// Completed function_call.arguments parse for the OpenAI Responses stream.
|
|
671
|
-
// A function_call item arrives only on a completion/done signal, so a
|
|
672
|
-
// non-empty-but-malformed
|
|
673
|
-
// arguments string is deterministic bad JSON — NOT mid-stream truncation.
|
|
674
|
-
// Empty/whitespace input legitimately means "no arguments" → {}. A non-empty
|
|
675
|
-
// string that fails JSON.parse is surfaced as an invalid-args MARKER (instead
|
|
676
|
-
// of being silently swallowed to {}) so the dispatch loop turns it into an
|
|
677
|
-
// is_error tool_result and the model self-corrects in the same turn.
|
|
678
|
-
function _parseJsonObject(value) {
|
|
679
|
-
const text = typeof value === 'string' ? value : (value == null ? '' : String(value));
|
|
680
|
-
if (text.trim() === '') return {};
|
|
681
|
-
try {
|
|
682
|
-
const parsed = JSON.parse(text);
|
|
683
|
-
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
684
|
-
} catch (err) {
|
|
685
|
-
return makeInvalidToolArgsMarker(text, err instanceof Error ? err.message : String(err));
|
|
686
|
-
}
|
|
646
|
+
return ordered;
|
|
687
647
|
}
|
|
688
648
|
|
|
689
|
-
|
|
690
|
-
const details = usage?.input_tokens_details || usage?.prompt_tokens_details || {};
|
|
691
|
-
return Number(details.cached_tokens ?? details.cached ?? usage?.cached_tokens ?? 0) || 0;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
function _sseEventsFromBuffer(buffer) {
|
|
695
|
-
const frames = [];
|
|
696
|
-
let rest = buffer.replace(/\r\n/g, '\n');
|
|
697
|
-
let idx;
|
|
698
|
-
while ((idx = rest.indexOf('\n\n')) >= 0) {
|
|
699
|
-
frames.push(rest.slice(0, idx));
|
|
700
|
-
rest = rest.slice(idx + 2);
|
|
701
|
-
}
|
|
702
|
-
return { frames, rest };
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
function _parseSseFrame(frame) {
|
|
706
|
-
const lines = String(frame || '').split('\n');
|
|
707
|
-
const data = [];
|
|
708
|
-
for (const line of lines) {
|
|
709
|
-
if (!line || line.startsWith(':')) continue;
|
|
710
|
-
if (line.startsWith('data:')) data.push(line.slice(5).trimStart());
|
|
711
|
-
}
|
|
712
|
-
if (!data.length) return null;
|
|
713
|
-
const raw = data.join('\n').trim();
|
|
714
|
-
if (!raw || raw === '[DONE]') return null;
|
|
715
|
-
try { return JSON.parse(raw); } catch { return null; }
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
function _incompleteReasonFromEvent(event) {
|
|
719
|
-
const reasonObj = event?.response?.incomplete_details
|
|
720
|
-
|| event?.incomplete_details
|
|
721
|
-
|| event?.response?.status_details
|
|
722
|
-
|| null;
|
|
723
|
-
return String(reasonObj?.reason || event?.response?.status || 'incomplete');
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
function _isMaxOutputIncompleteReason(reason) {
|
|
727
|
-
return /^(?:max_output_tokens|max_tokens|length|output_token_limit)$/i.test(String(reason || '').trim());
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
function _pushOutputTextAnnotations(part, citations, citationKeys) {
|
|
731
|
-
const annotations = Array.isArray(part?.annotations) ? part.annotations : [];
|
|
732
|
-
for (const raw of annotations) {
|
|
733
|
-
const url = raw?.url || raw?.uri || raw?.href || '';
|
|
734
|
-
if (!url || citationKeys.has(url)) continue;
|
|
735
|
-
citationKeys.add(url);
|
|
736
|
-
citations.push({
|
|
737
|
-
title: raw?.title || '',
|
|
738
|
-
url,
|
|
739
|
-
snippet: raw?.snippet || raw?.text || raw?.description || '',
|
|
740
|
-
source: 'openai-oauth',
|
|
741
|
-
});
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
function _buildOpenAIHttpFallbackHeaders({ auth, cacheKey }) {
|
|
746
|
-
const headers = {
|
|
747
|
-
Authorization: `Bearer ${auth.access_token}`,
|
|
748
|
-
'Content-Type': 'application/json',
|
|
749
|
-
Accept: 'text/event-stream',
|
|
750
|
-
'OpenAI-Beta': 'responses=experimental',
|
|
751
|
-
originator: CODEX_OAUTH_ORIGINATOR,
|
|
752
|
-
'chatgpt-account-id': auth.account_id || '',
|
|
753
|
-
'x-client-request-id': randomBytes(16).toString('hex'),
|
|
754
|
-
};
|
|
755
|
-
if (cacheKey) headers.session_id = String(cacheKey);
|
|
756
|
-
return headers;
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
// WS→HTTP/SSE fallback predicate → shared shouldFallbackTransport
|
|
760
|
-
// (retry-classifier.mjs). The per-provider env flag is computed here and passed
|
|
761
|
-
// as `enabled`; the deny-order + allow-list are identical to the former copy.
|
|
762
|
-
function _shouldUseOpenAIHttpFallback(err, externalSignal) {
|
|
763
|
-
return shouldFallbackTransport(err, {
|
|
764
|
-
signal: externalSignal,
|
|
765
|
-
enabled: _envFlag('MIXDOG_OPENAI_OAUTH_HTTP_FALLBACK', true),
|
|
766
|
-
});
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
// Exported for the single-emit regression smoke (scripts/openai-oauth-
|
|
770
|
-
// http-sse-toolcall-smoke.mjs): the SSE stream can surface the same
|
|
771
|
-
// function_call across response.function_call_arguments.done +
|
|
772
|
-
// response.output_item.done + response.completed, and onToolCall must fire
|
|
773
|
-
// exactly once per call id. No production caller imports this name; the
|
|
774
|
-
// provider invokes it internally.
|
|
775
|
-
export async function sendViaHttpSse({
|
|
776
|
-
auth,
|
|
777
|
-
body,
|
|
778
|
-
opts,
|
|
779
|
-
onStreamDelta,
|
|
780
|
-
onToolCall,
|
|
781
|
-
onTextDelta,
|
|
782
|
-
onStageChange,
|
|
783
|
-
externalSignal,
|
|
784
|
-
poolKey,
|
|
785
|
-
cacheKey,
|
|
786
|
-
iteration,
|
|
787
|
-
useModel,
|
|
788
|
-
fetchFn = fetch,
|
|
789
|
-
} = {}) {
|
|
790
|
-
// P1 audit fix: no fixed wall-clock total cap on the HTTP/SSE fallback
|
|
791
|
-
// stream. The old createTimeoutSignal(..., PROVIDER_GENERATE_TOTAL_TIMEOUT_MS)
|
|
792
|
-
// killed a healthy, still-streaming turn purely on elapsed time, unlike
|
|
793
|
-
// every other streaming provider path (anthropic-oauth uses the same
|
|
794
|
-
// createPassthroughSignal pattern — see anthropic-oauth.mjs "Option A").
|
|
795
|
-
// The stream is bounded instead by:
|
|
796
|
-
// (a) headerTimeout below (PROVIDER_HTTP_RESPONSE_TIMEOUT_MS) for a
|
|
797
|
-
// socket that never sends the initial response,
|
|
798
|
-
// (b) the SEMANTIC idle watchdog (_armSemanticIdle /
|
|
799
|
-
// PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS), which resets on every
|
|
800
|
-
// meaningful() chunk — a live stream stays alive, a truly silent
|
|
801
|
-
// one still aborts, and
|
|
802
|
-
// (c) externalSignal (client disconnect / replaced-by-newer-request).
|
|
803
|
-
const totalTimeout = createPassthroughSignal(externalSignal);
|
|
804
|
-
const headerTimeout = createTimeoutSignal(
|
|
805
|
-
totalTimeout.signal,
|
|
806
|
-
PROVIDER_HTTP_RESPONSE_TIMEOUT_MS,
|
|
807
|
-
'OpenAI OAuth HTTP fallback initial response',
|
|
808
|
-
);
|
|
809
|
-
const headers = _buildOpenAIHttpFallbackHeaders({ auth, cacheKey });
|
|
810
|
-
const fetchStartedAt = Date.now();
|
|
811
|
-
let response;
|
|
812
|
-
try {
|
|
813
|
-
try { onStageChange?.('requesting'); } catch {}
|
|
814
|
-
response = await fetchFn(CODEX_RESPONSES_URL, {
|
|
815
|
-
method: 'POST',
|
|
816
|
-
headers,
|
|
817
|
-
body: JSON.stringify(body),
|
|
818
|
-
signal: headerTimeout.signal,
|
|
819
|
-
dispatcher: getLlmDispatcher(),
|
|
820
|
-
});
|
|
821
|
-
} catch (err) {
|
|
822
|
-
if (headerTimeout.signal?.aborted && headerTimeout.signal.reason instanceof Error) throw headerTimeout.signal.reason;
|
|
823
|
-
throw err;
|
|
824
|
-
} finally {
|
|
825
|
-
headerTimeout.cleanup();
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
traceAgentFetch({
|
|
829
|
-
sessionId: poolKey,
|
|
830
|
-
headersMs: Date.now() - fetchStartedAt,
|
|
831
|
-
httpStatus: response.status,
|
|
832
|
-
provider: 'openai-oauth',
|
|
833
|
-
model: useModel,
|
|
834
|
-
transport: 'http',
|
|
835
|
-
});
|
|
836
|
-
|
|
837
|
-
if (!response.ok) {
|
|
838
|
-
const text = await response.text().catch(() => '');
|
|
839
|
-
const err = new Error(`OpenAI OAuth HTTP fallback ${response.status}: ${text.slice(0, 200)}`);
|
|
840
|
-
err.httpStatus = response.status;
|
|
841
|
-
err.headers = response.headers;
|
|
842
|
-
populateHttpStatusFromMessage(err, text);
|
|
843
|
-
totalTimeout.cleanup();
|
|
844
|
-
throw err;
|
|
845
|
-
}
|
|
846
|
-
if (!response.body) {
|
|
847
|
-
totalTimeout.cleanup();
|
|
848
|
-
throw new Error('OpenAI OAuth HTTP fallback returned no response body');
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
try { onStageChange?.('streaming'); } catch {}
|
|
852
|
-
const sseStartedAt = Date.now();
|
|
853
|
-
const reader = response.body.getReader();
|
|
854
|
-
const decoder = new TextDecoder();
|
|
855
|
-
// After headerTimeout.cleanup() the in-flight fetch no longer carries a live
|
|
856
|
-
// signal, so a totalTimeout / external abort that fires during a pending
|
|
857
|
-
// reader.read() would otherwise leave the pooled request hanging. Keep the
|
|
858
|
-
// reader tied to totalTimeout for the whole stream: on abort, cancel the
|
|
859
|
-
// reader so the awaited read() unblocks and the socket is released back to
|
|
860
|
-
// the shared pool instead of leaking. reader.cancel() may resolve the
|
|
861
|
-
// pending read() as {done:true} rather than rejecting, which would let a
|
|
862
|
-
// partial response surface as success — so record the abort reason and
|
|
863
|
-
// re-throw it after the loop unblocks (see below).
|
|
864
|
-
let _streamAbortReason = null;
|
|
865
|
-
let _onTotalAbort = null;
|
|
866
|
-
if (totalTimeout.signal) {
|
|
867
|
-
_onTotalAbort = () => {
|
|
868
|
-
const reason = totalTimeout.signal.reason;
|
|
869
|
-
_streamAbortReason = reason instanceof Error
|
|
870
|
-
? reason
|
|
871
|
-
: new Error('OpenAI OAuth HTTP fallback aborted');
|
|
872
|
-
try { reader.cancel(_streamAbortReason).catch(() => {}); } catch {}
|
|
873
|
-
};
|
|
874
|
-
if (totalTimeout.signal.aborted) _onTotalAbort();
|
|
875
|
-
else totalTimeout.signal.addEventListener('abort', _onTotalAbort, { once: true });
|
|
876
|
-
}
|
|
877
|
-
// SEMANTIC idle watchdog: reset ONLY on meaningful() (text/reasoning/tool
|
|
878
|
-
// deltas), never on raw bytes/keepalive frames, so a stream that emits some
|
|
879
|
-
// deltas then goes silent trips a short, named terminal failure instead of
|
|
880
|
-
// hanging until the 30-min agent watchdog. Disablable via the shared env.
|
|
881
|
-
let _semanticIdleTimer = null;
|
|
882
|
-
const _clearSemanticIdle = () => {
|
|
883
|
-
if (_semanticIdleTimer) { clearTimeout(_semanticIdleTimer); _semanticIdleTimer = null; }
|
|
884
|
-
};
|
|
885
|
-
const _armSemanticIdle = () => {
|
|
886
|
-
if (!PROVIDER_SSE_IDLE_WATCHDOG_ENABLED || !(PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS > 0)) return;
|
|
887
|
-
_clearSemanticIdle();
|
|
888
|
-
_semanticIdleTimer = setTimeout(() => {
|
|
889
|
-
_streamAbortReason = streamStalledError('OpenAI OAuth HTTP fallback', PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS, { emittedToolCall: emittedToolCallIds.size > 0 });
|
|
890
|
-
// Partial-final recovery: attach the
|
|
891
|
-
// streamed partial state so the agent loop can accept a wedged FINAL
|
|
892
|
-
// no-tool summary as a successful partial-final instead of dropping
|
|
893
|
-
// the result. pendingToolUse gates out any mid-flight tool call.
|
|
894
|
-
try {
|
|
895
|
-
_streamAbortReason.partialContent = content;
|
|
896
|
-
_streamAbortReason.partialToolCalls = toolCalls.length ? toolCalls.slice() : undefined;
|
|
897
|
-
_streamAbortReason.pendingToolUse = pendingCalls.size > 0 || emittedToolCallIds.size > 0;
|
|
898
|
-
_streamAbortReason.partialModel = model || undefined;
|
|
899
|
-
} catch { /* best-effort enrichment */ }
|
|
900
|
-
try { reader.cancel(_streamAbortReason).catch(() => {}); } catch {}
|
|
901
|
-
}, PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS);
|
|
902
|
-
try { _semanticIdleTimer.unref?.(); } catch {}
|
|
903
|
-
};
|
|
904
|
-
let buffer = '';
|
|
905
|
-
let content = '';
|
|
906
|
-
let model = '';
|
|
907
|
-
let responseId = '';
|
|
908
|
-
let serviceTier = '';
|
|
909
|
-
let usage = null;
|
|
910
|
-
let ttftMs = null;
|
|
911
|
-
const toolCalls = [];
|
|
912
|
-
const pendingCalls = new Map();
|
|
913
|
-
const reasoningItems = [];
|
|
914
|
-
const citations = [];
|
|
915
|
-
const citationKeys = new Set();
|
|
916
|
-
const webSearchCalls = [];
|
|
917
|
-
const webSearchCallKeys = new Set();
|
|
918
|
-
let completed = false;
|
|
919
|
-
let stopReason = null;
|
|
920
|
-
// Gateway live-text relay invariant: set once a non-empty text chunk has
|
|
921
|
-
// been forwarded to the client. A failure afterwards is non-retryable —
|
|
922
|
-
// the rendered text cannot be withdrawn and a re-request would concatenate
|
|
923
|
-
// a second attempt.
|
|
924
|
-
let emittedText = false;
|
|
925
|
-
|
|
926
|
-
// Single-emit guard for tool calls (matches the WS path's
|
|
927
|
-
// emittedToolCall intent). The HTTP/SSE event stream can surface the
|
|
928
|
-
// same function_call across multiple frames — response.function_call_arguments.done,
|
|
929
|
-
// response.output_item.done, and the final response.completed.output
|
|
930
|
-
// bundle. Each frame independently completes the call (id + name) and
|
|
931
|
-
// would re-invoke onToolCall, double-executing a side-effecting tool.
|
|
932
|
-
// Route every emit through emitToolCall: it fires the callback exactly
|
|
933
|
-
// once per unique call id, the first time the call is complete. A call
|
|
934
|
-
// whose id/name only arrives in a later frame is NOT dropped — its
|
|
935
|
-
// first complete frame still emits; only redundant re-emits are
|
|
936
|
-
// suppressed.
|
|
937
|
-
const emittedToolCallIds = new Set();
|
|
938
|
-
// Fix 2: cross-path name+args dedupe. A text-leaked synthetic and an
|
|
939
|
-
// identical native function_call must fire onToolCall exactly once.
|
|
940
|
-
const _toolDedupe = createToolCallDedupe();
|
|
941
|
-
const emitToolCall = (call) => {
|
|
942
|
-
if (!call || !call.id) return;
|
|
943
|
-
if (emittedToolCallIds.has(call.id)) return;
|
|
944
|
-
emittedToolCallIds.add(call.id);
|
|
945
|
-
if (!_toolDedupe.shouldDispatch(call.name, call.arguments)) return;
|
|
946
|
-
try { onToolCall?.(call); } catch {}
|
|
947
|
-
};
|
|
948
|
-
|
|
949
|
-
// Leaked tool-call guard. The model sometimes emits a tool call as plain
|
|
950
|
-
// text (XML `<invoke>`/`<function_calls>` or gpt-oss harmony
|
|
951
|
-
// `<|channel|>...to=functions.NAME...<|call|>`) inside
|
|
952
|
-
// `response.output_text.delta` instead of a native function_call. Route
|
|
953
|
-
// text through the guard so leaked calls are suppressed from the visible
|
|
954
|
-
// stream, synthesized (native `call_...` id shape), and dispatched like
|
|
955
|
-
// native ones. Known tool names come from the request body so recovery
|
|
956
|
-
// only fires for tools the model was actually offered. Additive: the
|
|
957
|
-
// native function_call path is untouched.
|
|
958
|
-
const _leakKnownTools = new Set(
|
|
959
|
-
(Array.isArray(body?.tools) ? body.tools : [])
|
|
960
|
-
.map((t) => (typeof t?.name === 'string' ? t.name : null))
|
|
961
|
-
.filter(Boolean),
|
|
962
|
-
);
|
|
963
|
-
const leakGuard = createLeakGuard({ knownToolNames: _leakKnownTools, harmony: true });
|
|
964
|
-
const dispatchLeakedCall = (recovered) => {
|
|
965
|
-
let args = recovered?.arguments;
|
|
966
|
-
if (args === null || typeof args !== 'object' || Array.isArray(args)) args = {};
|
|
967
|
-
const call = {
|
|
968
|
-
id: `call_leaked_${randomBytes(8).toString('hex')}`,
|
|
969
|
-
name: recovered.name,
|
|
970
|
-
arguments: args,
|
|
971
|
-
};
|
|
972
|
-
toolCalls.push(call);
|
|
973
|
-
emitToolCall(call);
|
|
974
|
-
};
|
|
975
|
-
const relayLeakText = (delta) => {
|
|
976
|
-
if (!leakGuard.enabled) {
|
|
977
|
-
content += delta || '';
|
|
978
|
-
if (delta && onTextDelta) {
|
|
979
|
-
emittedText = true;
|
|
980
|
-
try { onTextDelta(delta); } catch {}
|
|
981
|
-
}
|
|
982
|
-
return;
|
|
983
|
-
}
|
|
984
|
-
const { text, calls } = leakGuard.push(delta);
|
|
985
|
-
if (text) {
|
|
986
|
-
content += text;
|
|
987
|
-
if (onTextDelta) {
|
|
988
|
-
emittedText = true;
|
|
989
|
-
try { onTextDelta(text); } catch {}
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
for (const c of calls) dispatchLeakedCall(c);
|
|
993
|
-
};
|
|
994
|
-
const flushLeak = () => {
|
|
995
|
-
if (!leakGuard.enabled) return;
|
|
996
|
-
const { text, calls } = leakGuard.flush();
|
|
997
|
-
if (text) {
|
|
998
|
-
content += text;
|
|
999
|
-
if (onTextDelta) {
|
|
1000
|
-
emittedText = true;
|
|
1001
|
-
try { onTextDelta(text); } catch {}
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
for (const c of calls) dispatchLeakedCall(c);
|
|
1005
|
-
};
|
|
1006
|
-
|
|
1007
|
-
const pushWebSearchCall = (item) => {
|
|
1008
|
-
if (!item || item.type !== 'web_search_call') return;
|
|
1009
|
-
const key = item.id || JSON.stringify(item.action || item);
|
|
1010
|
-
if (webSearchCallKeys.has(key)) return;
|
|
1011
|
-
webSearchCallKeys.add(key);
|
|
1012
|
-
webSearchCalls.push({ id: item.id || '', status: item.status || '', action: item.action || null });
|
|
1013
|
-
};
|
|
1014
|
-
const pushReasoningItem = (item) => {
|
|
1015
|
-
if (item?.type === 'reasoning' && item.encrypted_content && !reasoningItems.some(r => r.id === item.id)) {
|
|
1016
|
-
reasoningItems.push({
|
|
1017
|
-
id: item.id || '',
|
|
1018
|
-
encrypted_content: item.encrypted_content,
|
|
1019
|
-
summary: Array.isArray(item.summary) ? item.summary : [],
|
|
1020
|
-
});
|
|
1021
|
-
}
|
|
1022
|
-
};
|
|
1023
|
-
const pushToolSearchCall = (item) => {
|
|
1024
|
-
if (!item || item.type !== 'tool_search_call') return;
|
|
1025
|
-
const callId = item.call_id || item.id || '';
|
|
1026
|
-
if (!callId || toolCalls.some(t => t.id === callId)) return;
|
|
1027
|
-
let args = {};
|
|
1028
|
-
if (item.arguments && typeof item.arguments === 'object') {
|
|
1029
|
-
args = item.arguments;
|
|
1030
|
-
} else if (typeof item.arguments === 'string' && item.arguments.trim()) {
|
|
1031
|
-
// Non-empty but malformed tool_search arguments are deterministic
|
|
1032
|
-
// bad JSON (the item is only emitted on completion). Surface an
|
|
1033
|
-
// invalid-args marker instead of swallowing to {} so the model can
|
|
1034
|
-
// self-correct in the same turn.
|
|
1035
|
-
args = _parseJsonObject(item.arguments);
|
|
1036
|
-
}
|
|
1037
|
-
const call = {
|
|
1038
|
-
id: callId,
|
|
1039
|
-
name: 'tool_search',
|
|
1040
|
-
arguments: args,
|
|
1041
|
-
nativeType: 'tool_search_call',
|
|
1042
|
-
};
|
|
1043
|
-
toolCalls.push(call);
|
|
1044
|
-
emitToolCall(call);
|
|
1045
|
-
};
|
|
1046
|
-
const pushCustomToolCall = (item) => {
|
|
1047
|
-
const call = customToolCallFromResponseItem(item);
|
|
1048
|
-
if (!call || toolCalls.some(t => t.id === call.id)) return;
|
|
1049
|
-
toolCalls.push(call);
|
|
1050
|
-
emitToolCall(call);
|
|
1051
|
-
};
|
|
1052
|
-
const meaningful = () => {
|
|
1053
|
-
if (ttftMs == null) ttftMs = Date.now() - sseStartedAt;
|
|
1054
|
-
_armSemanticIdle();
|
|
1055
|
-
try { onStreamDelta?.(); } catch {}
|
|
1056
|
-
};
|
|
1057
|
-
const handleEvent = (event) => {
|
|
1058
|
-
if (!event || typeof event.type !== 'string') return;
|
|
1059
|
-
switch (event.type) {
|
|
1060
|
-
case 'response.created':
|
|
1061
|
-
if (event.response?.model) model = event.response.model;
|
|
1062
|
-
if (event.response?.id) responseId = event.response.id;
|
|
1063
|
-
break;
|
|
1064
|
-
case 'response.output_text.delta':
|
|
1065
|
-
meaningful();
|
|
1066
|
-
relayLeakText(event.delta || '');
|
|
1067
|
-
break;
|
|
1068
|
-
case 'response.reasoning_text.delta':
|
|
1069
|
-
case 'response.reasoning_summary_text.delta':
|
|
1070
|
-
meaningful();
|
|
1071
|
-
break;
|
|
1072
|
-
case 'response.output_item.added':
|
|
1073
|
-
if (event.item?.type === 'function_call') {
|
|
1074
|
-
pendingCalls.set(event.item.id || '', {
|
|
1075
|
-
name: event.item.name || '',
|
|
1076
|
-
callId: event.item.call_id || '',
|
|
1077
|
-
});
|
|
1078
|
-
} else if (event.item?.type === 'tool_search_call') {
|
|
1079
|
-
// Mark tool_search as in-flight the moment the item is
|
|
1080
|
-
// added, mirroring function_call above, so the semantic
|
|
1081
|
-
// idle watchdog's pendingToolUse gate (pendingCalls.size)
|
|
1082
|
-
// sees a mid-flight tool_search and never lets stall
|
|
1083
|
-
// recovery drop it before response.output_item.done.
|
|
1084
|
-
// kind:'tool_search' tags the entry so the shared
|
|
1085
|
-
// function_call_arguments.done handler (below) never
|
|
1086
|
-
// mistakes it for a function call by id collision/empty id.
|
|
1087
|
-
if (event.item.id) {
|
|
1088
|
-
pendingCalls.set(event.item.id, {
|
|
1089
|
-
name: 'tool_search',
|
|
1090
|
-
callId: event.item.call_id || '',
|
|
1091
|
-
kind: 'tool_search',
|
|
1092
|
-
});
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
break;
|
|
1096
|
-
case 'response.function_call_arguments.delta':
|
|
1097
|
-
meaningful();
|
|
1098
|
-
break;
|
|
1099
|
-
case 'response.function_call_arguments.done': {
|
|
1100
|
-
const itemId = event.item_id || '';
|
|
1101
|
-
const pending = pendingCalls.get(itemId);
|
|
1102
|
-
if (pending?.kind === 'tool_search') { meaningful(); break; }
|
|
1103
|
-
const call = {
|
|
1104
|
-
id: pending?.callId || event.call_id || '',
|
|
1105
|
-
name: pending?.name || event.name || '',
|
|
1106
|
-
arguments: _parseJsonObject(event.arguments),
|
|
1107
|
-
_pendingItemId: itemId,
|
|
1108
|
-
};
|
|
1109
|
-
toolCalls.push(call);
|
|
1110
|
-
if (call.id && call.name) {
|
|
1111
|
-
delete call._pendingItemId;
|
|
1112
|
-
emitToolCall(call);
|
|
1113
|
-
}
|
|
1114
|
-
meaningful();
|
|
1115
|
-
break;
|
|
1116
|
-
}
|
|
1117
|
-
case 'response.custom_tool_call_input.delta':
|
|
1118
|
-
meaningful();
|
|
1119
|
-
break;
|
|
1120
|
-
case 'response.output_item.done': {
|
|
1121
|
-
const item = event.item || {};
|
|
1122
|
-
pushReasoningItem(item);
|
|
1123
|
-
pushWebSearchCall(item);
|
|
1124
|
-
if (item.type === 'function_call') {
|
|
1125
|
-
const tc = toolCalls.find(t => t._pendingItemId === (item.id || ''));
|
|
1126
|
-
if (tc) {
|
|
1127
|
-
if (!tc.id && item.call_id) tc.id = item.call_id;
|
|
1128
|
-
if (!tc.name && item.name) tc.name = item.name;
|
|
1129
|
-
if (tc.id && tc.name) {
|
|
1130
|
-
delete tc._pendingItemId;
|
|
1131
|
-
emitToolCall(tc);
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
} else if (item.type === 'tool_search_call') {
|
|
1135
|
-
pendingCalls.delete(item.id || '');
|
|
1136
|
-
pushToolSearchCall(item);
|
|
1137
|
-
} else if (item.type === 'custom_tool_call') {
|
|
1138
|
-
pushCustomToolCall(item);
|
|
1139
|
-
meaningful();
|
|
1140
|
-
}
|
|
1141
|
-
break;
|
|
1142
|
-
}
|
|
1143
|
-
case 'response.completed': {
|
|
1144
|
-
const resp = event.response || {};
|
|
1145
|
-
serviceTier = resp.service_tier || resp.serviceTier || serviceTier;
|
|
1146
|
-
if (!model && resp.model) model = resp.model;
|
|
1147
|
-
if (!responseId && resp.id) responseId = resp.id;
|
|
1148
|
-
if (resp.usage) {
|
|
1149
|
-
usage = {
|
|
1150
|
-
inputTokens: resp.usage.input_tokens || 0,
|
|
1151
|
-
outputTokens: resp.usage.output_tokens || 0,
|
|
1152
|
-
cachedTokens: _extractCachedTokens(resp.usage),
|
|
1153
|
-
promptTokens: resp.usage.input_tokens || 0,
|
|
1154
|
-
raw: serviceTier ? { ...resp.usage, service_tier: serviceTier } : resp.usage,
|
|
1155
|
-
};
|
|
1156
|
-
}
|
|
1157
|
-
for (const item of resp.output || []) {
|
|
1158
|
-
if (item.type === 'message') {
|
|
1159
|
-
for (const part of item.content || []) {
|
|
1160
|
-
if (!content && part.type === 'output_text') {
|
|
1161
|
-
// Completed-output fallback (no streamed text).
|
|
1162
|
-
// Route through the leak guard so a tool call
|
|
1163
|
-
// leaked only in the final bundle is recovered
|
|
1164
|
-
// rather than surfaced as visible content. push
|
|
1165
|
-
// with final=true flushes fully (no held tail).
|
|
1166
|
-
if (leakGuard.enabled) {
|
|
1167
|
-
const { text, calls } = leakGuard.push(part.text || '', true);
|
|
1168
|
-
content += text;
|
|
1169
|
-
for (const c of calls) dispatchLeakedCall(c);
|
|
1170
|
-
} else {
|
|
1171
|
-
content += part.text || '';
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
if (part.type === 'output_text') _pushOutputTextAnnotations(part, citations, citationKeys);
|
|
1175
|
-
}
|
|
1176
|
-
} else if (item.type === 'reasoning') {
|
|
1177
|
-
pushReasoningItem(item);
|
|
1178
|
-
} else if (item.type === 'web_search_call') {
|
|
1179
|
-
pushWebSearchCall(item);
|
|
1180
|
-
} else if (item.type === 'tool_search_call') {
|
|
1181
|
-
pushToolSearchCall(item);
|
|
1182
|
-
} else if (item.type === 'custom_tool_call') {
|
|
1183
|
-
pushCustomToolCall(item);
|
|
1184
|
-
meaningful();
|
|
1185
|
-
} else if (item.type === 'function_call') {
|
|
1186
|
-
// Match the still-pending placeholder by item id, or
|
|
1187
|
-
// an already-recorded call by its canonical call_id —
|
|
1188
|
-
// so a call completed at args.done / output_item.done
|
|
1189
|
-
// is reused here rather than re-pushed as a duplicate.
|
|
1190
|
-
const tc = toolCalls.find(t =>
|
|
1191
|
-
t._pendingItemId === (item.id || '')
|
|
1192
|
-
|| (item.call_id && t.id === item.call_id));
|
|
1193
|
-
if (tc) {
|
|
1194
|
-
if (!tc.id && item.call_id) tc.id = item.call_id;
|
|
1195
|
-
if (!tc.name && item.name) tc.name = item.name;
|
|
1196
|
-
if (tc.id && tc.name) {
|
|
1197
|
-
delete tc._pendingItemId;
|
|
1198
|
-
emitToolCall(tc);
|
|
1199
|
-
}
|
|
1200
|
-
} else if (item.call_id && item.name) {
|
|
1201
|
-
const call = {
|
|
1202
|
-
id: item.call_id,
|
|
1203
|
-
name: item.name,
|
|
1204
|
-
arguments: _parseJsonObject(item.arguments),
|
|
1205
|
-
};
|
|
1206
|
-
toolCalls.push(call);
|
|
1207
|
-
emitToolCall(call);
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
completed = true;
|
|
1212
|
-
break;
|
|
1213
|
-
}
|
|
1214
|
-
case 'response.done':
|
|
1215
|
-
if (!event.response || event.response.status === 'completed') completed = true;
|
|
1216
|
-
else if (event.response.status === 'failed') {
|
|
1217
|
-
const msg = event.response?.error?.message || 'response.done failed';
|
|
1218
|
-
const err = new Error(`OpenAI OAuth HTTP fallback response.done failed: ${msg}`);
|
|
1219
|
-
populateHttpStatusFromMessage(err, msg);
|
|
1220
|
-
throw err;
|
|
1221
|
-
} else if (event.response.status === 'incomplete') {
|
|
1222
|
-
const reason = _incompleteReasonFromEvent(event);
|
|
1223
|
-
if (_isMaxOutputIncompleteReason(reason)) {
|
|
1224
|
-
completed = true;
|
|
1225
|
-
stopReason = 'length';
|
|
1226
|
-
break;
|
|
1227
|
-
}
|
|
1228
|
-
throw new Error(`OpenAI OAuth HTTP fallback response.done incomplete: ${reason}`);
|
|
1229
|
-
}
|
|
1230
|
-
break;
|
|
1231
|
-
case 'response.failed': {
|
|
1232
|
-
const msg = event.response?.error?.message || event.error?.message || event.message || 'response.failed';
|
|
1233
|
-
const err = new Error(`OpenAI OAuth HTTP fallback response.failed: ${msg}`);
|
|
1234
|
-
populateHttpStatusFromMessage(err, msg);
|
|
1235
|
-
throw err;
|
|
1236
|
-
}
|
|
1237
|
-
case 'response.incomplete': {
|
|
1238
|
-
const reason = _incompleteReasonFromEvent(event);
|
|
1239
|
-
if (_isMaxOutputIncompleteReason(reason)) {
|
|
1240
|
-
completed = true;
|
|
1241
|
-
stopReason = 'length';
|
|
1242
|
-
break;
|
|
1243
|
-
}
|
|
1244
|
-
throw new Error(`OpenAI OAuth HTTP fallback response.incomplete: ${reason}`);
|
|
1245
|
-
}
|
|
1246
|
-
case 'error': {
|
|
1247
|
-
const msg = event.message || event.error?.message || 'unknown';
|
|
1248
|
-
const err = new Error(`OpenAI OAuth HTTP fallback error: ${msg}`);
|
|
1249
|
-
populateHttpStatusFromMessage(err, msg);
|
|
1250
|
-
throw err;
|
|
1251
|
-
}
|
|
1252
|
-
default:
|
|
1253
|
-
break;
|
|
1254
|
-
}
|
|
1255
|
-
};
|
|
1256
|
-
|
|
1257
|
-
try {
|
|
1258
|
-
while (true) {
|
|
1259
|
-
if (totalTimeout.signal?.aborted) {
|
|
1260
|
-
_clearSemanticIdle();
|
|
1261
|
-
const reason = totalTimeout.signal.reason;
|
|
1262
|
-
throw reason instanceof Error ? reason : new Error('OpenAI OAuth HTTP fallback aborted');
|
|
1263
|
-
}
|
|
1264
|
-
if (_streamAbortReason) throw _streamAbortReason;
|
|
1265
|
-
const { value, done } = await reader.read();
|
|
1266
|
-
if (done) break;
|
|
1267
|
-
buffer += decoder.decode(value, { stream: true });
|
|
1268
|
-
const parsed = _sseEventsFromBuffer(buffer);
|
|
1269
|
-
buffer = parsed.rest;
|
|
1270
|
-
for (const frame of parsed.frames) {
|
|
1271
|
-
const event = _parseSseFrame(frame);
|
|
1272
|
-
if (event) handleEvent(event);
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
// The read() above can unblock via reader.cancel() as {done:true} on an
|
|
1276
|
-
// external/total-timeout abort. Surface that as the abort/timeout error
|
|
1277
|
-
// instead of treating the partial stream as a successful response.
|
|
1278
|
-
if (_streamAbortReason) throw _streamAbortReason;
|
|
1279
|
-
buffer += decoder.decode();
|
|
1280
|
-
const parsed = _sseEventsFromBuffer(buffer + '\n\n');
|
|
1281
|
-
for (const frame of parsed.frames) {
|
|
1282
|
-
const event = _parseSseFrame(frame);
|
|
1283
|
-
if (event) handleEvent(event);
|
|
1284
|
-
}
|
|
1285
|
-
// Flush any partial-sentinel tail held back mid-stream so legitimate
|
|
1286
|
-
// trailing text is never lost (streamed-text path).
|
|
1287
|
-
flushLeak();
|
|
1288
|
-
} catch (err) {
|
|
1289
|
-
// Live-text invariant: once a non-empty chunk has been relayed it
|
|
1290
|
-
// cannot be withdrawn — flag the error so no upstream layer retries.
|
|
1291
|
-
if (emittedText && err) { try { err.liveTextEmitted = true; err.unsafeToRetry = true; } catch {} }
|
|
1292
|
-
throw err;
|
|
1293
|
-
} finally {
|
|
1294
|
-
_clearSemanticIdle();
|
|
1295
|
-
try { reader.releaseLock?.(); } catch {}
|
|
1296
|
-
if (_onTotalAbort && totalTimeout.signal) {
|
|
1297
|
-
try { totalTimeout.signal.removeEventListener('abort', _onTotalAbort); } catch {}
|
|
1298
|
-
}
|
|
1299
|
-
totalTimeout.cleanup();
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
const unresolved = toolCalls.find(t => t._pendingItemId);
|
|
1303
|
-
if (unresolved) {
|
|
1304
|
-
throw new Error(`OpenAI OAuth HTTP fallback function_call salvage failed: missing call_id/name for item_id=${unresolved._pendingItemId || '?'}`);
|
|
1305
|
-
}
|
|
1306
|
-
if (!completed && !content && !toolCalls.length) {
|
|
1307
|
-
throw new Error('OpenAI OAuth HTTP fallback ended before response.completed');
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
const liveModel = model || useModel;
|
|
1311
|
-
traceAgentSse({
|
|
1312
|
-
sessionId: poolKey,
|
|
1313
|
-
sseParseMs: Date.now() - sseStartedAt,
|
|
1314
|
-
ttftMs,
|
|
1315
|
-
provider: 'openai-oauth',
|
|
1316
|
-
model: liveModel,
|
|
1317
|
-
transport: 'sse',
|
|
1318
|
-
});
|
|
1319
|
-
if (usage) {
|
|
1320
|
-
traceAgentUsage({
|
|
1321
|
-
sessionId: poolKey,
|
|
1322
|
-
iteration,
|
|
1323
|
-
inputTokens: usage.inputTokens || 0,
|
|
1324
|
-
outputTokens: usage.outputTokens || 0,
|
|
1325
|
-
cachedTokens: usage.cachedTokens || 0,
|
|
1326
|
-
promptTokens: usage.promptTokens || 0,
|
|
1327
|
-
model: liveModel,
|
|
1328
|
-
modelDisplay: _displayCodexModel(liveModel),
|
|
1329
|
-
responseId: responseId || null,
|
|
1330
|
-
rawUsage: usage.raw || null,
|
|
1331
|
-
provider: 'openai-oauth',
|
|
1332
|
-
serviceTier,
|
|
1333
|
-
});
|
|
1334
|
-
}
|
|
1335
|
-
// Dedupe the returned array by name+args (Fix 2, array side): a synthetic
|
|
1336
|
-
// leaked call and an identical native function_call must not both survive,
|
|
1337
|
-
// else the agent loop executes the side-effecting tool twice.
|
|
1338
|
-
const _returnedToolCalls = toolCalls.length
|
|
1339
|
-
? dedupeToolCallList(toolCalls.map(({ _pendingItemId, ...t }) => t))
|
|
1340
|
-
: undefined;
|
|
1341
|
-
return {
|
|
1342
|
-
content,
|
|
1343
|
-
model: liveModel,
|
|
1344
|
-
reasoningItems: reasoningItems.length ? reasoningItems : undefined,
|
|
1345
|
-
toolCalls: _returnedToolCalls,
|
|
1346
|
-
citations: citations.length ? citations : undefined,
|
|
1347
|
-
webSearchCalls: webSearchCalls.length ? webSearchCalls : undefined,
|
|
1348
|
-
usage: usage || undefined,
|
|
1349
|
-
stopReason: stopReason || undefined,
|
|
1350
|
-
// P1 audit fix: text-only max-output cutoff (openai-oauth HTTP/SSE
|
|
1351
|
-
// fallback maps status:'incomplete'/reason=max_output_tokens to
|
|
1352
|
-
// stopReason='length' above and treats it as success). Flag it so
|
|
1353
|
-
// loop.mjs can surface a truncation warning instead of accepting
|
|
1354
|
-
// silently-cut content as a clean final answer.
|
|
1355
|
-
...(stopReason === 'length' && content.length > 0 ? { truncated: true } : {}),
|
|
1356
|
-
responseId: responseId || undefined,
|
|
1357
|
-
serviceTier: serviceTier || undefined,
|
|
1358
|
-
};
|
|
1359
|
-
}
|
|
649
|
+
// --- HTTP/SSE fallback transport: extracted to openai-oauth-http-sse.mjs ---
|
|
1360
650
|
|
|
1361
651
|
// --- Provider ---
|
|
1362
652
|
export class OpenAIOAuthProvider {
|
|
@@ -1507,18 +797,12 @@ export class OpenAIOAuthProvider {
|
|
|
1507
797
|
const _authP = this.ensureAuth();
|
|
1508
798
|
let auth = await _authP;
|
|
1509
799
|
const body = await _bodyP;
|
|
1510
|
-
// poolKey
|
|
1511
|
-
// poolKey is per-session so parallel reviewer/worker callers each
|
|
1512
|
-
//
|
|
1513
|
-
//
|
|
1514
|
-
//
|
|
1515
|
-
//
|
|
1516
|
-
// `body.prompt_cache_key` and the handshake `session_id` header, so
|
|
1517
|
-
// compatible prefixes share cache without main/worker lanes evicting
|
|
1518
|
-
// each other.
|
|
1519
|
-
// poolKey defaults to sessionId (per-session socket isolation); cacheKey
|
|
1520
|
-
// never falls back to sessionId, so a fresh session still reuses the
|
|
1521
|
-
// warm prefix cache for the same route/prefix.
|
|
800
|
+
// poolKey != cacheKey by design (see openai-oauth-ws.mjs header note).
|
|
801
|
+
// poolKey is per-session so parallel reviewer/worker callers each get
|
|
802
|
+
// their own socket bucket. cacheKey is the Codex-style prompt_cache_key:
|
|
803
|
+
// by default it is the session/thread identity (clamped to 64 chars) and
|
|
804
|
+
// feeds both `body.prompt_cache_key` and the OAuth WS handshake
|
|
805
|
+
// `session_id`, so each long-lived thread keeps a stable cache shard.
|
|
1522
806
|
const poolKey = opts.sessionId || null;
|
|
1523
807
|
const cacheKey = body.prompt_cache_key || resolveProviderCacheKey(opts, 'openai-oauth');
|
|
1524
808
|
const iteration = Number.isFinite(Number(opts.iteration)) ? Number(opts.iteration) : null;
|
|
@@ -1734,7 +1018,7 @@ export class OpenAIOAuthProvider {
|
|
|
1734
1018
|
const items = Array.isArray(data?.models) ? data.models : [];
|
|
1735
1019
|
const normalized = items.map(m => _normalizeCodexModel(m));
|
|
1736
1020
|
_markLatestCodex(normalized);
|
|
1737
|
-
const enriched = await enrichModels(normalized);
|
|
1021
|
+
const enriched = sanitizeModelList((await enrichModels(normalized)).filter(Boolean), { provider: 'openai-oauth' });
|
|
1738
1022
|
await _saveCodexModelCache(enriched);
|
|
1739
1023
|
_lastCodexListModelsError = '';
|
|
1740
1024
|
return enriched;
|
|
@@ -1772,7 +1056,7 @@ export class OpenAIOAuthProvider {
|
|
|
1772
1056
|
const items = Array.isArray(data?.models) ? data.models : [];
|
|
1773
1057
|
const normalized = items.map(m => _normalizeCodexModel(m));
|
|
1774
1058
|
_markLatestCodex(normalized);
|
|
1775
|
-
const enriched = await enrichModels(normalized);
|
|
1059
|
+
const enriched = sanitizeModelList((await enrichModels(normalized)).filter(Boolean), { provider: 'openai-oauth' });
|
|
1776
1060
|
await _saveCodexModelCache(enriched);
|
|
1777
1061
|
if (!process.env.MIXDOG_QUIET_PROVIDER_LOG) process.stderr.write(`[openai-oauth] catalog refreshed (${enriched.length} models)\n`);
|
|
1778
1062
|
return enriched;
|
|
@@ -1791,168 +1075,12 @@ export class OpenAIOAuthProvider {
|
|
|
1791
1075
|
}
|
|
1792
1076
|
}
|
|
1793
1077
|
|
|
1794
|
-
|
|
1795
|
-
const
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
function generatePKCE() {
|
|
1804
|
-
const verifier = randomBytes(64).toString('base64url');
|
|
1805
|
-
const challenge = createHash('sha256').update(verifier).digest('base64url');
|
|
1806
|
-
return { verifier, challenge };
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
|
-
function _scrubOAuthLoginBody(text) {
|
|
1810
|
-
return String(text || '')
|
|
1811
|
-
.replace(/"access_token"\s*:\s*"[^"]+"/g, '"access_token":"[REDACTED]"')
|
|
1812
|
-
.replace(/"refresh_token"\s*:\s*"[^"]+"/g, '"refresh_token":"[REDACTED]"')
|
|
1813
|
-
.replace(/"id_token"\s*:\s*"[^"]+"/g, '"id_token":"[REDACTED]"')
|
|
1814
|
-
.replace(/[A-Za-z0-9_-]{32,}\.[A-Za-z0-9._-]+/g, '[REDACTED]');
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1817
|
-
function _parseOAuthCodeInput(input) {
|
|
1818
|
-
const value = String(input || '').trim();
|
|
1819
|
-
if (!value) return { code: '', state: '' };
|
|
1820
|
-
try {
|
|
1821
|
-
const url = new URL(value);
|
|
1822
|
-
const code = url.searchParams.get('code') || '';
|
|
1823
|
-
const state = url.searchParams.get('state') || '';
|
|
1824
|
-
if (code || state) return { code, state };
|
|
1825
|
-
} catch { /* not a URL */ }
|
|
1826
|
-
if (value.includes('#')) {
|
|
1827
|
-
const [code, state] = value.split('#', 2);
|
|
1828
|
-
return { code: String(code || '').trim(), state: String(state || '').trim() };
|
|
1829
|
-
}
|
|
1830
|
-
if (value.includes('code=')) {
|
|
1831
|
-
const params = new URLSearchParams(value.startsWith('?') ? value.slice(1) : value);
|
|
1832
|
-
return { code: params.get('code') || '', state: params.get('state') || '' };
|
|
1833
|
-
}
|
|
1834
|
-
return { code: value, state: '' };
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
async function exchangeAuthorizationCode({ pkce, code }) {
|
|
1838
|
-
const cleanCode = String(code || '').trim();
|
|
1839
|
-
if (!cleanCode) throw new Error('[openai-oauth] authorization code is required');
|
|
1840
|
-
const tokenRes = await fetch(TOKEN_URL, {
|
|
1841
|
-
method: 'POST',
|
|
1842
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
1843
|
-
body: new URLSearchParams({
|
|
1844
|
-
grant_type: 'authorization_code',
|
|
1845
|
-
code: cleanCode,
|
|
1846
|
-
redirect_uri: REDIRECT_URI,
|
|
1847
|
-
client_id: CLIENT_ID,
|
|
1848
|
-
code_verifier: pkce.verifier,
|
|
1849
|
-
}),
|
|
1850
|
-
redirect: 'error',
|
|
1851
|
-
signal: AbortSignal.timeout(TOKEN_TIMEOUT_MS),
|
|
1852
|
-
});
|
|
1853
|
-
if (!tokenRes.ok) {
|
|
1854
|
-
const text = await tokenRes.text().catch(() => '');
|
|
1855
|
-
throw new Error(`[openai-oauth] token exchange ${tokenRes.status}: ${_scrubOAuthLoginBody(text).slice(0, 500)}`);
|
|
1856
|
-
}
|
|
1857
|
-
const json = await tokenRes.json();
|
|
1858
|
-
if (!json.access_token || !json.refresh_token) {
|
|
1859
|
-
throw new Error('[openai-oauth] token exchange response missing access_token or refresh_token');
|
|
1860
|
-
}
|
|
1861
|
-
const expiresAt = (typeof json.expires_in === 'number'
|
|
1862
|
-
? Date.now() + json.expires_in * 1000
|
|
1863
|
-
: 0) || _expiryFromAccessToken(json.access_token);
|
|
1864
|
-
const tokens = {
|
|
1865
|
-
access_token: json.access_token,
|
|
1866
|
-
refresh_token: json.refresh_token,
|
|
1867
|
-
expires_at: expiresAt,
|
|
1868
|
-
account_id: extractAccountId(json.access_token),
|
|
1869
|
-
};
|
|
1870
|
-
saveTokens(tokens);
|
|
1871
|
-
return tokens;
|
|
1872
|
-
}
|
|
1873
|
-
|
|
1874
|
-
export async function beginOAuthLogin() {
|
|
1875
|
-
const pkce = generatePKCE();
|
|
1876
|
-
const state = randomBytes(16).toString('hex');
|
|
1877
|
-
const url = new URL(AUTHORIZE_URL);
|
|
1878
|
-
url.searchParams.set('response_type', 'code');
|
|
1879
|
-
url.searchParams.set('client_id', CLIENT_ID);
|
|
1880
|
-
url.searchParams.set('redirect_uri', REDIRECT_URI);
|
|
1881
|
-
url.searchParams.set('scope', CODEX_OAUTH_SCOPE);
|
|
1882
|
-
url.searchParams.set('code_challenge', pkce.challenge);
|
|
1883
|
-
url.searchParams.set('code_challenge_method', 'S256');
|
|
1884
|
-
url.searchParams.set('id_token_add_organizations', 'true');
|
|
1885
|
-
url.searchParams.set('codex_cli_simplified_flow', 'true');
|
|
1886
|
-
url.searchParams.set('state', state);
|
|
1887
|
-
url.searchParams.set('originator', CODEX_OAUTH_ORIGINATOR);
|
|
1888
|
-
|
|
1889
|
-
let server = null;
|
|
1890
|
-
let timeout = null;
|
|
1891
|
-
let finish = null;
|
|
1892
|
-
const waitForCallback = new Promise((resolve, reject) => {
|
|
1893
|
-
let settled = false;
|
|
1894
|
-
finish = (value, error = null) => {
|
|
1895
|
-
if (settled) return;
|
|
1896
|
-
settled = true;
|
|
1897
|
-
if (timeout) clearTimeout(timeout);
|
|
1898
|
-
try { server?.close(); } catch { /* already closed */ }
|
|
1899
|
-
if (error) reject(error);
|
|
1900
|
-
else resolve(value);
|
|
1901
|
-
};
|
|
1902
|
-
server = createServer(async (req, res) => {
|
|
1903
|
-
const u = new URL(req.url || '/', `http://${CALLBACK_HOST}:${CALLBACK_PORT}`);
|
|
1904
|
-
if (u.pathname !== CALLBACK_PATH) {
|
|
1905
|
-
res.writeHead(404);
|
|
1906
|
-
res.end();
|
|
1907
|
-
return;
|
|
1908
|
-
}
|
|
1909
|
-
const code = u.searchParams.get('code');
|
|
1910
|
-
if (!code || u.searchParams.get('state') !== state) {
|
|
1911
|
-
res.writeHead(400);
|
|
1912
|
-
res.end('Invalid');
|
|
1913
|
-
finish(null);
|
|
1914
|
-
return;
|
|
1915
|
-
}
|
|
1916
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1917
|
-
res.end('<html><body><h2>OpenAI OAuth login successful! You can close this tab.</h2></body></html>');
|
|
1918
|
-
try {
|
|
1919
|
-
const tokens = await exchangeAuthorizationCode({ pkce, code });
|
|
1920
|
-
finish(tokens);
|
|
1921
|
-
} catch (err) {
|
|
1922
|
-
finish(null, err instanceof Error ? err : new Error(String(err)));
|
|
1923
|
-
}
|
|
1924
|
-
});
|
|
1925
|
-
timeout = setTimeout(() => finish(null), LOGIN_TIMEOUT_MS);
|
|
1926
|
-
server.listen(CALLBACK_PORT, CALLBACK_HOST, async () => {
|
|
1927
|
-
process.stderr.write(`\n[openai-oauth] Open this URL to log in to ChatGPT (OpenAI OAuth):\n${url.toString()}\n\n`);
|
|
1928
|
-
try {
|
|
1929
|
-
const { openInBrowser } = await import('../../../shared/open-url.mjs');
|
|
1930
|
-
openInBrowser(url.toString());
|
|
1931
|
-
} catch (err) {
|
|
1932
|
-
process.stderr.write(`[openai-oauth] browser open failed: ${String(err?.message || err).slice(0, 200)}\n`);
|
|
1933
|
-
}
|
|
1934
|
-
});
|
|
1935
|
-
server.on('error', (err) => finish(null, new Error(`[openai-oauth] callback server failed on ${CALLBACK_HOST}:${CALLBACK_PORT}: ${err?.message || err}`)));
|
|
1936
|
-
});
|
|
1937
|
-
|
|
1938
|
-
return {
|
|
1939
|
-
provider: 'openai-oauth',
|
|
1940
|
-
url: url.toString(),
|
|
1941
|
-
waitForCallback,
|
|
1942
|
-
completeCode: async (input) => {
|
|
1943
|
-
const parsed = _parseOAuthCodeInput(input);
|
|
1944
|
-
if (parsed.state && parsed.state !== state) throw new Error('[openai-oauth] OAuth state mismatch');
|
|
1945
|
-
const tokens = await exchangeAuthorizationCode({ pkce, code: parsed.code });
|
|
1946
|
-
finish?.(tokens);
|
|
1947
|
-
return tokens;
|
|
1948
|
-
},
|
|
1949
|
-
cancel: () => {
|
|
1950
|
-
finish?.(null);
|
|
1951
|
-
},
|
|
1952
|
-
};
|
|
1953
|
-
}
|
|
1954
|
-
|
|
1955
|
-
export async function loginOAuth() {
|
|
1956
|
-
const login = await beginOAuthLogin();
|
|
1957
|
-
return await login.waitForCallback;
|
|
1958
|
-
}
|
|
1078
|
+
// --- OAuth PKCE login flow: extracted to openai-oauth-login.mjs ---
|
|
1079
|
+
const { beginOAuthLogin, loginOAuth } = createOpenAIOAuthLogin({
|
|
1080
|
+
clientId: CLIENT_ID,
|
|
1081
|
+
originator: CODEX_OAUTH_ORIGINATOR,
|
|
1082
|
+
extractAccountId,
|
|
1083
|
+
expiryFromAccessToken: _expiryFromAccessToken,
|
|
1084
|
+
saveTokens,
|
|
1085
|
+
});
|
|
1086
|
+
export { beginOAuthLogin, loginOAuth };
|