@cartanova/qgrid-cli 2.10.10 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/dist/_virtual/_rolldown/runtime.js +1 -10
- package/bundle/dist/application/monit/log-buffer.js +12 -6
- package/bundle/dist/application/monit/monit.frame.js +11 -3
- package/bundle/dist/application/monit/monit.types.js +1 -1
- package/bundle/dist/application/qgrid/conv-routing.js +9 -6
- package/bundle/dist/application/qgrid/expired-token-reminder.js +2 -2
- package/bundle/dist/application/qgrid/oauth.js +31 -4
- package/bundle/dist/application/qgrid/qgrid-image-generation.js +1 -1
- package/bundle/dist/application/qgrid/qgrid-response-format.js +21 -19
- package/bundle/dist/application/qgrid/qgrid-run-lifecycle.js +40 -33
- package/bundle/dist/application/qgrid/qgrid.dispatcher.js +128 -93
- package/bundle/dist/application/qgrid/qgrid.frame.js +930 -49
- package/bundle/dist/application/qgrid/qgrid.types.js +212 -5
- package/bundle/dist/application/qgrid/schema-prompt.js +75 -48
- package/bundle/dist/application/qgrid/slack-user-map.js +1 -1
- package/bundle/dist/application/qgrid/token-death.js +1 -1
- package/bundle/dist/application/qgrid/token-subscriber.js +83 -30
- package/bundle/dist/application/qgrid/token-trigger-setup.js +1 -1
- package/bundle/dist/application/qgrid/token-window-keepalive.js +8 -6
- package/bundle/dist/application/qgrid/tool-emulation-schema.js +13 -13
- package/bundle/dist/application/qgrid/tool-emulation.js +19 -15
- package/bundle/dist/application/request-log/json-schema-type-text.js +29 -12
- package/bundle/dist/application/request-log/request-log.model.js +83 -70
- package/bundle/dist/application/request-log/request-log.types.js +1 -1
- package/bundle/dist/application/request-log/zod-code-format.js +1 -1
- package/bundle/dist/application/request-log-step/request-log-step.model.js +1 -1
- package/bundle/dist/application/request-log-step/request-log-step.types.js +1 -1
- package/bundle/dist/application/setting/setting.constant.js +1 -1
- package/bundle/dist/application/setting/setting.model.js +2 -2
- package/bundle/dist/application/setting/setting.store.js +2 -2
- package/bundle/dist/application/setting/setting.types.js +1 -1
- package/bundle/dist/application/sonamu.generated.js +32 -16
- package/bundle/dist/application/sonamu.generated.sso.js +1 -1
- package/bundle/dist/application/token/token.model.js +26 -9
- package/bundle/dist/application/token/token.types.js +20 -3
- package/bundle/dist/i18n/en.js +15 -1
- package/bundle/dist/i18n/ko.js +15 -1
- package/bundle/dist/i18n/sd.generated.js +1 -1
- package/bundle/dist/migrations/20260807133553_manual__backfill_request_log_cost.js +8 -6
- package/bundle/dist/server-bootstrap.js +4 -2
- package/bundle/dist/server-error-handler.js +1 -1
- package/bundle/dist/sonamu.config.js +37 -8
- package/bundle/dist/startup-migrations.js +17 -7
- package/bundle/dist/testing/global.js +1 -1
- package/bundle/dist/testing/schema-bench.js +0 -0
- package/bundle/dist/testing/test-database-name.js +1 -1
- package/bundle/dist/utils/providers/anthropic/anthropic-constants.js +1 -1
- package/bundle/dist/utils/providers/anthropic/anthropic-dispatcher.js +11 -5
- package/bundle/dist/utils/providers/anthropic/anthropic-quota.js +4 -4
- package/bundle/dist/utils/providers/anthropic/anthropic-thinking.js +1 -1
- package/bundle/dist/utils/providers/anthropic/claude-session.js +20 -19
- package/bundle/dist/utils/providers/anthropic/fence-strip.js +1 -1
- package/bundle/dist/utils/providers/anthropic/stream-json-adapter.js +49 -23
- package/bundle/dist/utils/providers/common/credentials.js +5 -1
- package/bundle/dist/utils/providers/common/effort.js +3 -3
- package/bundle/dist/utils/providers/common/model-cost.js +69 -14
- package/bundle/dist/utils/providers/common/provider-types.js +29 -1
- package/bundle/dist/utils/providers/common/schema-validation.js +78 -40
- package/bundle/dist/utils/providers/common/smooth-weighted-round-robin.js +1 -1
- package/bundle/dist/utils/providers/common/strictifier.js +62 -43
- package/bundle/dist/utils/providers/common/thinking.js +3 -2
- package/bundle/dist/utils/providers/common/ttft.js +1 -1
- package/bundle/dist/utils/providers/grok/grok-backend-protocol.js +427 -0
- package/bundle/dist/utils/providers/grok/grok-constants.js +55 -0
- package/bundle/dist/utils/providers/grok/grok-direct-client.js +196 -0
- package/bundle/dist/utils/providers/grok/grok-dispatcher.js +1313 -0
- package/bundle/dist/utils/providers/grok/grok-headers.js +118 -0
- package/bundle/dist/utils/providers/grok/grok-media-client.js +420 -0
- package/bundle/dist/utils/providers/grok/grok-models.js +114 -0
- package/bundle/dist/utils/providers/grok/grok-oauth.js +455 -0
- package/bundle/dist/utils/providers/grok/grok-quota.js +119 -0
- package/bundle/dist/utils/providers/grok/grok-refresh.js +192 -0
- package/bundle/dist/utils/providers/grok/grok-types.js +186 -0
- package/bundle/dist/utils/providers/grok/grok-voice-client.js +199 -0
- package/bundle/dist/utils/providers/grok/index.js +11 -0
- package/bundle/dist/utils/providers/openai/openai-backend-protocol.js +111 -83
- package/bundle/dist/utils/providers/openai/openai-callback-relay.js +3 -2
- package/bundle/dist/utils/providers/openai/openai-constants.js +1 -1
- package/bundle/dist/utils/providers/openai/openai-direct-client.js +23 -7
- package/bundle/dist/utils/providers/openai/openai-dispatcher.js +143 -106
- package/bundle/dist/utils/providers/openai/openai-image-generation.js +48 -33
- package/bundle/dist/utils/providers/openai/openai-oauth.js +28 -13
- package/bundle/dist/utils/providers/openai/openai-quota.js +38 -27
- package/bundle/dist/utils/providers/openai/openai-refresh.js +17 -9
- package/bundle/dist/utils/providers/openai/openai-sse.js +1 -1
- package/bundle/dist/utils/providers/openai/openai-transport-config.js +1 -1
- package/bundle/dist/utils/quiet-hours.js +1 -1
- package/bundle/dist/utils/slack-notify.js +14 -8
- package/bundle/dist/utils/subset-loaders.js +4 -5
- package/bundle/src/application/monit/log-buffer.ts +23 -9
- package/bundle/src/application/monit/monit.frame.ts +22 -5
- package/bundle/src/application/monit/monit.types.ts +6 -0
- package/bundle/src/application/qgrid/conv-routing.test.ts +76 -4
- package/bundle/src/application/qgrid/conv-routing.ts +35 -9
- package/bundle/src/application/qgrid/expired-token-reminder.ts +9 -1
- package/bundle/src/application/qgrid/oauth.ts +51 -15
- package/bundle/src/application/qgrid/qgrid-image-generation.ts +9 -0
- package/bundle/src/application/qgrid/qgrid-response-format.ts +50 -25
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.test.ts +19 -0
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +84 -40
- package/bundle/src/application/qgrid/qgrid.dispatcher.test.ts +97 -0
- package/bundle/src/application/qgrid/qgrid.dispatcher.ts +274 -126
- package/bundle/src/application/qgrid/qgrid.frame.test.ts +1013 -3
- package/bundle/src/application/qgrid/qgrid.frame.ts +1415 -60
- package/bundle/src/application/qgrid/qgrid.types.ts +335 -0
- package/bundle/src/application/qgrid/schema-prompt.ts +105 -41
- package/bundle/src/application/qgrid/slack-user-map.ts +5 -0
- package/bundle/src/application/qgrid/token-death.ts +4 -0
- package/bundle/src/application/qgrid/token-subscriber.test.ts +21 -19
- package/bundle/src/application/qgrid/token-subscriber.ts +186 -53
- package/bundle/src/application/qgrid/token-trigger-setup.ts +2 -0
- package/bundle/src/application/qgrid/token-window-keepalive.ts +49 -6
- package/bundle/src/application/qgrid/tool-emulation-schema.ts +33 -29
- package/bundle/src/application/qgrid/tool-emulation.ts +52 -27
- package/bundle/src/application/request-log/json-schema-type-text.test.ts +5 -5
- package/bundle/src/application/request-log/json-schema-type-text.ts +81 -26
- package/bundle/src/application/request-log/request-log.model.test.ts +111 -4
- package/bundle/src/application/request-log/request-log.model.ts +277 -146
- package/bundle/src/application/request-log/request-log.types.ts +6 -0
- package/bundle/src/application/request-log/zod-code-format.ts +11 -0
- package/bundle/src/application/request-log-step/request-log-step.model.ts +1 -0
- package/bundle/src/application/request-log-step/request-log-step.types.ts +2 -0
- package/bundle/src/application/setting/setting.constant.ts +9 -0
- package/bundle/src/application/setting/setting.model.ts +12 -2
- package/bundle/src/application/setting/setting.store.ts +5 -1
- package/bundle/src/application/setting/setting.types.ts +8 -0
- package/bundle/src/application/sonamu.generated.http +133 -0
- package/bundle/src/application/sonamu.generated.sso.ts +2 -0
- package/bundle/src/application/sonamu.generated.ts +15 -0
- package/bundle/src/application/token/token.identity.test.ts +20 -0
- package/bundle/src/application/token/token.model.ts +66 -7
- package/bundle/src/application/token/token.types.ts +24 -1
- package/bundle/src/i18n/en.ts +19 -0
- package/bundle/src/i18n/ko.ts +21 -0
- package/bundle/src/i18n/sd.generated.ts +23 -2
- package/bundle/src/migrations/20260807133553_manual__backfill_request_log_cost.ts +16 -8
- package/bundle/src/server-bootstrap.ts +4 -2
- package/bundle/src/server-error-handler.ts +1 -0
- package/bundle/src/sonamu.config.ts +69 -7
- package/bundle/src/startup-migrations.ts +27 -9
- package/bundle/src/testing/global.ts +20 -0
- package/bundle/src/testing/schema-bench.ts +105 -30
- package/bundle/src/testing/test-database-name.ts +2 -0
- package/bundle/src/utils/providers/anthropic/anthropic-constants.ts +8 -0
- package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.ts +41 -6
- package/bundle/src/utils/providers/anthropic/anthropic-quota.ts +7 -3
- package/bundle/src/utils/providers/anthropic/anthropic-thinking.ts +6 -0
- package/bundle/src/utils/providers/anthropic/claude-session.ts +58 -24
- package/bundle/src/utils/providers/anthropic/fence-strip.ts +11 -0
- package/bundle/src/utils/providers/anthropic/stream-json-adapter.ts +110 -45
- package/bundle/src/utils/providers/common/credentials.ts +8 -0
- package/bundle/src/utils/providers/common/effort.ts +9 -2
- package/bundle/src/utils/providers/common/model-cost.test.ts +48 -1
- package/bundle/src/utils/providers/common/model-cost.ts +259 -158
- package/bundle/src/utils/providers/common/provider-dispatcher.ts +98 -2
- package/bundle/src/utils/providers/common/provider-types.ts +46 -3
- package/bundle/src/utils/providers/common/schema-validation.test.ts +5 -1
- package/bundle/src/utils/providers/common/schema-validation.ts +124 -56
- package/bundle/src/utils/providers/common/smooth-weighted-round-robin.ts +4 -0
- package/bundle/src/utils/providers/common/strictifier.ts +185 -59
- package/bundle/src/utils/providers/common/thinking.ts +2 -2
- package/bundle/src/utils/providers/common/ttft.ts +4 -2
- package/bundle/src/utils/providers/grok/__fixtures__/golden-responses-1.0.60.json +164 -0
- package/bundle/src/utils/providers/grok/grok-backend-protocol.test.ts +325 -0
- package/bundle/src/utils/providers/grok/grok-backend-protocol.ts +721 -0
- package/bundle/src/utils/providers/grok/grok-constants.ts +105 -0
- package/bundle/src/utils/providers/grok/grok-direct-client.test.ts +179 -0
- package/bundle/src/utils/providers/grok/grok-direct-client.ts +313 -0
- package/bundle/src/utils/providers/grok/grok-dispatcher.test.ts +1395 -0
- package/bundle/src/utils/providers/grok/grok-dispatcher.ts +2105 -0
- package/bundle/src/utils/providers/grok/grok-headers.test.ts +149 -0
- package/bundle/src/utils/providers/grok/grok-headers.ts +199 -0
- package/bundle/src/utils/providers/grok/grok-media-client.test.ts +446 -0
- package/bundle/src/utils/providers/grok/grok-media-client.ts +703 -0
- package/bundle/src/utils/providers/grok/grok-models.test.ts +96 -0
- package/bundle/src/utils/providers/grok/grok-models.ts +161 -0
- package/bundle/src/utils/providers/grok/grok-oauth.test.ts +406 -0
- package/bundle/src/utils/providers/grok/grok-oauth.ts +771 -0
- package/bundle/src/utils/providers/grok/grok-quota.test.ts +142 -0
- package/bundle/src/utils/providers/grok/grok-quota.ts +198 -0
- package/bundle/src/utils/providers/grok/grok-refresh.test.ts +239 -0
- package/bundle/src/utils/providers/grok/grok-refresh.ts +325 -0
- package/bundle/src/utils/providers/grok/grok-types.test.ts +252 -0
- package/bundle/src/utils/providers/grok/grok-types.ts +374 -0
- package/bundle/src/utils/providers/grok/grok-voice-client.test.ts +321 -0
- package/bundle/src/utils/providers/grok/grok-voice-client.ts +284 -0
- package/bundle/src/utils/providers/grok/index.ts +20 -0
- package/bundle/src/utils/providers/openai/openai-backend-protocol.ts +221 -137
- package/bundle/src/utils/providers/openai/openai-callback-relay.ts +12 -1
- package/bundle/src/utils/providers/openai/openai-constants.ts +2 -0
- package/bundle/src/utils/providers/openai/openai-direct-client.ts +111 -21
- package/bundle/src/utils/providers/openai/openai-dispatcher.ts +322 -169
- package/bundle/src/utils/providers/openai/openai-image-generation.ts +116 -36
- package/bundle/src/utils/providers/openai/openai-oauth.ts +48 -20
- package/bundle/src/utils/providers/openai/openai-quota.ts +77 -46
- package/bundle/src/utils/providers/openai/openai-refresh.ts +42 -18
- package/bundle/src/utils/providers/openai/openai-sse.ts +27 -3
- package/bundle/src/utils/providers/openai/openai-transport-config.ts +2 -0
- package/bundle/src/utils/quiet-hours.ts +12 -1
- package/bundle/src/utils/slack-notify.ts +44 -13
- package/bundle/src/utils/subset-loaders.ts +7 -7
- package/bundle/web-dist/client/assets/clsx-5XM_pHvN.js +1 -0
- package/bundle/web-dist/client/assets/index-BA7uQ-6c.js +31 -0
- package/bundle/web-dist/client/assets/index-Be2qCZmI.css +1 -0
- package/bundle/web-dist/client/assets/logs-C47OJGEI.js +1 -0
- package/bundle/web-dist/client/assets/monit-BS9no0dU.js +4 -0
- package/bundle/web-dist/client/assets/plus-sdACTfH0.js +1 -0
- package/bundle/web-dist/client/assets/refresh-cw-CY8o_1cT.js +1 -0
- package/bundle/web-dist/client/assets/request-model-BRtFuZvj.js +1 -0
- package/bundle/web-dist/client/assets/routes-kqgkUxE8.js +5 -0
- package/bundle/web-dist/client/assets/{services.generated-NP-ValHd.js → services.generated-CefiDXEv.js} +5 -5
- package/bundle/web-dist/client/assets/settings-DNDvFGSw.js +1 -0
- package/bundle/web-dist/client/assets/show-u8Np69q3.js +2 -0
- package/bundle/web-dist/client/assets/tokens-QvP410NW.js +1 -0
- package/bundle/web-dist/client/assets/total-cost-MnfKU8IN.js +1 -0
- package/bundle/web-dist/client/assets/use-oauth-login-flow-Bzdh28Ws.js +1 -0
- package/bundle/web-dist/client/assets/use-update-token-mutation-CfJKaxmR.js +1 -0
- package/bundle/web-dist/client/assets/vendor-react-CUFFrVN9.js +244 -0
- package/bundle/web-dist/client/assets/vendor-tanstack-Bf_jag9G.js +1 -0
- package/bundle/web-dist/client/index.html +6 -6
- package/bundle/web-dist/server/assets/clsx-D_N5PN_2.js +16 -0
- package/bundle/web-dist/server/assets/{lazyRouteComponent-CgBdabgd.js → lazyRouteComponent-D8iTIHyn.js} +129 -2
- package/bundle/web-dist/server/assets/logs-B3OTd9hy.js +825 -0
- package/bundle/web-dist/server/assets/{monit-ZPt66W5M.js → monit-B2hqorPe.js} +385 -126
- package/bundle/web-dist/server/assets/plus-DwG7h9kq.js +28 -0
- package/bundle/web-dist/server/assets/{react-dom-Ds75e4Jl.js → react-dom-BIYQzBqG.js} +2 -129
- package/bundle/web-dist/server/assets/refresh-cw-mMBkwOq0.js +44 -0
- package/bundle/web-dist/server/assets/{request-model-DjGldb-h.js → request-model-I6OdODSH.js} +15 -6
- package/bundle/web-dist/server/assets/{routes-Ct-k7nBA.js → routes-B95YlUv2.js} +760 -213
- package/bundle/web-dist/server/assets/{services.generated-BA4AcAf5.js → services.generated-DQATJ6qf.js} +12820 -12194
- package/bundle/web-dist/server/assets/settings-C3gbGk0m.js +830 -0
- package/bundle/web-dist/server/assets/{show-LSFIzvO2.js → show-Bxaj3rhH.js} +1350 -418
- package/bundle/web-dist/server/assets/tokens-CZjH3cB7.js +1038 -0
- package/bundle/web-dist/server/assets/total-cost-fTE9Xy82.js +18 -0
- package/bundle/web-dist/server/assets/use-oauth-login-flow-CKDHgc3U.js +2379 -0
- package/bundle/web-dist/server/assets/{use-update-token-mutation-pkArIzCP.js → use-update-token-mutation-BEbgOzaa.js} +1 -1
- package/bundle/web-dist/server/entry-server.generated.js +4295 -22810
- package/dist/cli.js +9 -9
- package/package.json +4 -11
- package/postinstall.mjs +12 -0
- package/skills/qgrid/SKILL.md +1 -0
- package/skills/qgrid/references/cli-env-and-server-boot.md +4 -1
- package/skills/qgrid/references/decision-rationale.md +23 -0
- package/skills/qgrid/references/grok-runtime.md +89 -0
- package/skills/qgrid/references/provider-runtime-differences.md +30 -29
- package/skills/qgrid/references/repo-map.md +10 -0
- package/skills/qgrid/references/token-auth-quota-lifecycle.md +56 -3
- package/bundle/dist/node_modules/.pnpm/@jridgewell_sourcemap-codec@1.5.5/node_modules/@jridgewell/sourcemap-codec/dist/sourcemap-codec.js +0 -406
- package/bundle/dist/node_modules/.pnpm/@vitest_expect@4.1.10/node_modules/@vitest/expect/dist/index.js +0 -1507
- package/bundle/dist/node_modules/.pnpm/@vitest_pretty-format@4.1.10/node_modules/@vitest/pretty-format/dist/index.js +0 -887
- package/bundle/dist/node_modules/.pnpm/@vitest_runner@4.1.10/node_modules/@vitest/runner/dist/chunk-artifact.js +0 -2756
- package/bundle/dist/node_modules/.pnpm/@vitest_runner@4.1.10/node_modules/@vitest/runner/dist/index.js +0 -8
- package/bundle/dist/node_modules/.pnpm/@vitest_runner@4.1.10/node_modules/@vitest/runner/dist/utils.js +0 -8
- package/bundle/dist/node_modules/.pnpm/@vitest_snapshot@4.1.10/node_modules/@vitest/snapshot/dist/index.js +0 -922
- package/bundle/dist/node_modules/.pnpm/@vitest_spy@4.1.10/node_modules/@vitest/spy/dist/index.js +0 -386
- package/bundle/dist/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/chunk-pathe.M-eThtNZ.js +0 -121
- package/bundle/dist/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/constants.js +0 -51
- package/bundle/dist/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/diff.js +0 -1317
- package/bundle/dist/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/display.js +0 -561
- package/bundle/dist/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/error.js +0 -40
- package/bundle/dist/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/helpers.js +0 -272
- package/bundle/dist/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/offset.js +0 -27
- package/bundle/dist/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/serialize.js +0 -77
- package/bundle/dist/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/source-map.js +0 -381
- package/bundle/dist/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/timers.js +0 -51
- package/bundle/dist/node_modules/.pnpm/chai@6.2.2/node_modules/chai/index.js +0 -2986
- package/bundle/dist/node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/branding.js +0 -10
- package/bundle/dist/node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/index.js +0 -109
- package/bundle/dist/node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/messages.js +0 -10
- package/bundle/dist/node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/overloads.js +0 -10
- package/bundle/dist/node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/utils.js +0 -10
- package/bundle/dist/node_modules/.pnpm/magic-string@0.30.21/node_modules/magic-string/dist/magic-string.es.js +0 -1126
- package/bundle/dist/node_modules/.pnpm/pathe@2.0.3/node_modules/pathe/dist/index.js +0 -21
- package/bundle/dist/node_modules/.pnpm/pathe@2.0.3/node_modules/pathe/dist/shared/pathe.M-eThtNZ.js +0 -432
- package/bundle/dist/node_modules/.pnpm/tinybench@2.9.0/node_modules/tinybench/dist/index.js +0 -517
- package/bundle/dist/node_modules/.pnpm/tinyrainbow@3.1.0/node_modules/tinyrainbow/dist/index.js +0 -101
- package/bundle/dist/node_modules/.pnpm/vite@8.0.3_@emnapi_core@1.11.2_@emnapi_runtime@1.11.2_@types_node@25.0.7_esbuild@0.27.4_5b35fc1fb85d2af4d105e0d74f940f50/node_modules/vite/dist/node/module-runner.js +0 -1265
- package/bundle/dist/node_modules/.pnpm/vitest@4.1.10_@opentelemetry_api@1.9.0_@types_node@25.0.7_@vitest_coverage-v8@4.1.10_vi_a1977b93af7bb69308bcb1dbab39b618/node_modules/vitest/dist/chunks/_commonjsHelpers.D26ty3Ew.js +0 -9
- package/bundle/dist/node_modules/.pnpm/vitest@4.1.10_@opentelemetry_api@1.9.0_@types_node@25.0.7_@vitest_coverage-v8@4.1.10_vi_a1977b93af7bb69308bcb1dbab39b618/node_modules/vitest/dist/chunks/benchmark.CX_oY03V.js +0 -41
- package/bundle/dist/node_modules/.pnpm/vitest@4.1.10_@opentelemetry_api@1.9.0_@types_node@25.0.7_@vitest_coverage-v8@4.1.10_vi_a1977b93af7bb69308bcb1dbab39b618/node_modules/vitest/dist/chunks/evaluatedModules.Dg1zASAC.js +0 -19
- package/bundle/dist/node_modules/.pnpm/vitest@4.1.10_@opentelemetry_api@1.9.0_@types_node@25.0.7_@vitest_coverage-v8@4.1.10_vi_a1977b93af7bb69308bcb1dbab39b618/node_modules/vitest/dist/chunks/index.Chj8NDwU.js +0 -202
- package/bundle/dist/node_modules/.pnpm/vitest@4.1.10_@opentelemetry_api@1.9.0_@types_node@25.0.7_@vitest_coverage-v8@4.1.10_vi_a1977b93af7bb69308bcb1dbab39b618/node_modules/vitest/dist/chunks/index.DdgEv5B1.js +0 -46
- package/bundle/dist/node_modules/.pnpm/vitest@4.1.10_@opentelemetry_api@1.9.0_@types_node@25.0.7_@vitest_coverage-v8@4.1.10_vi_a1977b93af7bb69308bcb1dbab39b618/node_modules/vitest/dist/chunks/rpc.MzXet3jl.js +0 -119
- package/bundle/dist/node_modules/.pnpm/vitest@4.1.10_@opentelemetry_api@1.9.0_@types_node@25.0.7_@vitest_coverage-v8@4.1.10_vi_a1977b93af7bb69308bcb1dbab39b618/node_modules/vitest/dist/chunks/test.DNmyFkvJ.js +0 -3044
- package/bundle/dist/node_modules/.pnpm/vitest@4.1.10_@opentelemetry_api@1.9.0_@types_node@25.0.7_@vitest_coverage-v8@4.1.10_vi_a1977b93af7bb69308bcb1dbab39b618/node_modules/vitest/dist/chunks/utils.BX5Fg8C4.js +0 -59
- package/bundle/dist/node_modules/.pnpm/vitest@4.1.10_@opentelemetry_api@1.9.0_@types_node@25.0.7_@vitest_coverage-v8@4.1.10_vi_a1977b93af7bb69308bcb1dbab39b618/node_modules/vitest/dist/index.js +0 -28
- package/bundle/dist/testing/setup-mocks.js +0 -16
- package/bundle/src/testing/setup-mocks.ts +0 -44
- package/bundle/web-dist/client/assets/index-Bw2s88Ot.css +0 -1
- package/bundle/web-dist/client/assets/index-DZwzUfI-.js +0 -106
- package/bundle/web-dist/client/assets/logs-Cog79DU5.js +0 -1
- package/bundle/web-dist/client/assets/monit-2auznBys.js +0 -1
- package/bundle/web-dist/client/assets/plus-B62CuqT8.js +0 -1
- package/bundle/web-dist/client/assets/refresh-cw-NNcU0Cmq.js +0 -1
- package/bundle/web-dist/client/assets/request-model-CojtinqK.js +0 -1
- package/bundle/web-dist/client/assets/routes-B0lTeLK-.js +0 -5
- package/bundle/web-dist/client/assets/settings-D8ZxZGSJ.js +0 -1
- package/bundle/web-dist/client/assets/show-CFSERlnM.js +0 -2
- package/bundle/web-dist/client/assets/tokens-CZXUyI7O.js +0 -1
- package/bundle/web-dist/client/assets/use-oauth-login-flow-hboOs3wG.js +0 -1
- package/bundle/web-dist/client/assets/use-update-token-mutation-BQHS0byG.js +0 -1
- package/bundle/web-dist/client/assets/vendor-react-DH4kUfK2.js +0 -9
- package/bundle/web-dist/client/assets/vendor-tanstack-DJRQAbrY.js +0 -1
- package/bundle/web-dist/server/assets/logs-BMyR98rU.js +0 -509
- package/bundle/web-dist/server/assets/plus-BZ9I0VgC.js +0 -19
- package/bundle/web-dist/server/assets/refresh-cw-CztCFis-.js +0 -23
- package/bundle/web-dist/server/assets/settings-CBUZ47Xx.js +0 -461
- package/bundle/web-dist/server/assets/tokens-DxrBW512.js +0 -448
- package/bundle/web-dist/server/assets/use-oauth-login-flow-Gn8flMK5.js +0 -178
- package/dist/cli.d.ts +0 -10
- /package/bundle/web-dist/client/assets/{cost-DH2jZbrw.js → cost-DB3HbU7T.js} +0 -0
- /package/bundle/web-dist/client/assets/{preload-helper-ca-nBW7U.js → preload-helper-DWTEM3RW.js} +0 -0
- /package/bundle/web-dist/server/assets/{cost-eEHeTRT5.js → cost-BC9yzyA9.js} +0 -0
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { getLogger } from "@logtape/logtape";
|
|
2
|
+
import { z } from "zod";
|
|
2
3
|
|
|
3
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
type ImageGenerationMetadata,
|
|
6
|
+
QuotaThresholdExceededError,
|
|
7
|
+
} from "../../../application/qgrid/qgrid.types";
|
|
4
8
|
import { TokenModel } from "../../../application/token/token.model";
|
|
5
9
|
import { type OpenAICredentials } from "../../../application/token/token.types";
|
|
10
|
+
import { isOpenAICredentials } from "../common/credentials";
|
|
6
11
|
import { resolveOpenAIEffort } from "../common/effort";
|
|
7
12
|
import {
|
|
8
13
|
type GenerateRequest,
|
|
@@ -11,11 +16,13 @@ import {
|
|
|
11
16
|
type GeneratedImage,
|
|
12
17
|
type ProviderDispatcher,
|
|
13
18
|
} from "../common/provider-dispatcher";
|
|
19
|
+
import { isJsonObject, jsonString } from "../common/provider-types";
|
|
14
20
|
import { SmoothWeightedRoundRobin } from "../common/smooth-weighted-round-robin";
|
|
15
21
|
import { ThinkingValidationError } from "../common/thinking";
|
|
16
22
|
import {
|
|
17
23
|
imageMimeType,
|
|
18
24
|
type JsonValue,
|
|
25
|
+
type OpenAINormalizedEvent,
|
|
19
26
|
type OpenAIResponseItem,
|
|
20
27
|
type OpenAIResponsesOptions,
|
|
21
28
|
} from "./openai-backend-protocol";
|
|
@@ -25,6 +32,7 @@ import { handleChatgptAuthTokensRefresh } from "./openai-refresh";
|
|
|
25
32
|
import { type OpenAITransportKind, resolveOpenAITransportKind } from "./openai-transport-config";
|
|
26
33
|
|
|
27
34
|
const logger = getLogger(["qgrid", "openai-dispatcher"]);
|
|
35
|
+
|
|
28
36
|
// Verified with the subscription Responses backend; public API support is not sufficient.
|
|
29
37
|
const THINKING_OFF_MODELS = new Set([
|
|
30
38
|
"gpt-6-sol",
|
|
@@ -34,6 +42,7 @@ const THINKING_OFF_MODELS = new Set([
|
|
|
34
42
|
"gpt-5.6-luna",
|
|
35
43
|
"gpt-5.5",
|
|
36
44
|
]);
|
|
45
|
+
|
|
37
46
|
const DEFAULT_REQUEST_TIMEOUT_MS = 600_000;
|
|
38
47
|
const IMAGE_TOOL = "generate_image";
|
|
39
48
|
|
|
@@ -43,6 +52,27 @@ type ImageToolArguments = {
|
|
|
43
52
|
num_last_images_to_include: number;
|
|
44
53
|
};
|
|
45
54
|
|
|
55
|
+
// Exactly these three keys; the backend rejects strict tools, so arguments are validated here.
|
|
56
|
+
const ImageToolArgumentsSchema = z.strictObject({
|
|
57
|
+
prompt: z.string().refine((prompt) => prompt.trim().length > 0),
|
|
58
|
+
transparent_background: z.boolean(),
|
|
59
|
+
num_last_images_to_include: z.number().int().min(0).max(5),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
function generatedImageFrom(
|
|
63
|
+
event: Extract<OpenAINormalizedEvent, { type: "image" }>,
|
|
64
|
+
): GeneratedImage {
|
|
65
|
+
const image: GeneratedImage = {
|
|
66
|
+
data: event.base64,
|
|
67
|
+
mediaType: event.mimeType,
|
|
68
|
+
revisedPrompt: event.revisedPrompt ?? null,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (event.generation) image.generation = event.generation;
|
|
72
|
+
|
|
73
|
+
return image;
|
|
74
|
+
}
|
|
75
|
+
|
|
46
76
|
export type ImageFailureKind = "gate" | "not_called" | "incomplete";
|
|
47
77
|
|
|
48
78
|
export class ImageGenerationError extends Error {
|
|
@@ -88,139 +118,161 @@ export interface OpenAIDispatcherDependencies {
|
|
|
88
118
|
|
|
89
119
|
function abortError(signal: AbortSignal): Error {
|
|
90
120
|
const reason = signal.reason;
|
|
121
|
+
|
|
91
122
|
return reason instanceof Error ? reason : new DOMException("Aborted", "AbortError");
|
|
92
123
|
}
|
|
93
124
|
|
|
94
125
|
function activeSignal(signal?: AbortSignal, timeoutMs?: number): AbortSignal {
|
|
95
126
|
const timeout = AbortSignal.timeout(timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS);
|
|
127
|
+
|
|
96
128
|
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
97
129
|
}
|
|
98
130
|
|
|
99
131
|
function quotaMessage(entries: Array<{ name: string; threshold: number }>): string {
|
|
100
132
|
const detail = entries.map((e) => `${e.name} (threshold ${e.threshold}%)`).join(", ");
|
|
133
|
+
|
|
101
134
|
return detail
|
|
102
135
|
? `All openai tokens exceeded quota threshold: ${detail}`
|
|
103
136
|
: "All openai tokens exceeded quota threshold";
|
|
104
137
|
}
|
|
105
138
|
|
|
106
|
-
function inputItems(req: GenerateRequest):
|
|
107
|
-
const history =
|
|
139
|
+
function inputItems(req: GenerateRequest): JsonValue[] {
|
|
140
|
+
const history = req.coldHistory ?? [];
|
|
141
|
+
|
|
108
142
|
const content: OpenAIResponseItem[] = req.coldInput.map((input) => {
|
|
109
143
|
if (input.type === "text") return { type: "input_text", text: input.text };
|
|
144
|
+
|
|
110
145
|
if (input.type === "image") return { type: "input_image", image_url: input.url };
|
|
146
|
+
|
|
111
147
|
if (input.type === "localImage") return { type: "input_image", image_url: input.path };
|
|
148
|
+
|
|
112
149
|
return { type: "input_text", text: `${input.name}: ${input.path}` };
|
|
113
150
|
});
|
|
151
|
+
|
|
114
152
|
return [...history, { type: "message", role: "user", content }];
|
|
115
153
|
}
|
|
116
154
|
|
|
117
155
|
function imageReferences(req: GenerateRequest): OpenAIResponseItem[] {
|
|
118
156
|
return inputItems(req).flatMap((item) => {
|
|
119
|
-
if (item
|
|
157
|
+
if (!isJsonObject(item)) return [];
|
|
158
|
+
const result = jsonString(item.result);
|
|
159
|
+
|
|
160
|
+
if (item.type === "image_generation_call" && result !== undefined) {
|
|
120
161
|
return [
|
|
121
162
|
{
|
|
122
163
|
type: "input_image",
|
|
123
|
-
image_url: `data:${imageMimeType(
|
|
164
|
+
image_url: `data:${imageMimeType(result, item.output_format)};base64,${result}`,
|
|
124
165
|
},
|
|
125
166
|
];
|
|
126
167
|
}
|
|
168
|
+
|
|
127
169
|
if (!Array.isArray(item.content)) return [];
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
(part.type === "input_image" || part.type === "image_url"),
|
|
133
|
-
);
|
|
170
|
+
|
|
171
|
+
return item.content
|
|
172
|
+
.filter(isJsonObject)
|
|
173
|
+
.filter((part) => part.type === "input_image" || part.type === "image_url");
|
|
134
174
|
});
|
|
135
175
|
}
|
|
136
176
|
|
|
137
|
-
|
|
177
|
+
type OpenAIReasoning = NonNullable<OpenAIResponsesOptions["reasoning"]>;
|
|
178
|
+
|
|
179
|
+
function asReasoning(req: GenerateRequest): OpenAIReasoning | undefined {
|
|
138
180
|
if (req.thinking === false) return { effort: "none" };
|
|
181
|
+
|
|
139
182
|
const summary =
|
|
140
183
|
req.reasoningSummary && req.reasoningSummary !== "none" ? req.reasoningSummary : undefined;
|
|
184
|
+
|
|
141
185
|
const effort =
|
|
142
186
|
resolveOpenAIEffort(req.model ?? "", req.effort) ?? (req.thinking === true ? "low" : undefined);
|
|
187
|
+
|
|
143
188
|
if (!effort && !summary) return undefined;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
189
|
+
|
|
190
|
+
const reasoning: OpenAIReasoning = {};
|
|
191
|
+
|
|
192
|
+
if (effort) reasoning.effort = effort;
|
|
193
|
+
|
|
194
|
+
if (summary) reasoning.summary = summary;
|
|
195
|
+
|
|
196
|
+
return reasoning;
|
|
152
197
|
}
|
|
153
198
|
|
|
199
|
+
const AUTO_IMAGE_INSTRUCTIONS =
|
|
200
|
+
"Use generate_image when image generation or editing is needed; otherwise reply with text. Call it at most once per response. Supply a complete, self-contained prompt with all relevant conversation instructions. Set transparent_background to true only when a transparent background is requested. Set num_last_images_to_include to 0 for a new image without references. For editing or reference-based generation, choose the smallest number of recent conversation images that includes all needed references, at most 5. References retain their chronological order. Do not select images for an unrelated new request.";
|
|
201
|
+
|
|
202
|
+
const AUTO_IMAGE_TOOL: OpenAIResponseItem = {
|
|
203
|
+
type: "function",
|
|
204
|
+
name: IMAGE_TOOL,
|
|
205
|
+
description:
|
|
206
|
+
"Generate or edit an image, optionally with a transparent background or recent conversation images as references.",
|
|
207
|
+
// Astra's subscription backend rejects strict tools; validate arguments locally.
|
|
208
|
+
strict: false,
|
|
209
|
+
parameters: {
|
|
210
|
+
type: "object",
|
|
211
|
+
properties: {
|
|
212
|
+
prompt: {
|
|
213
|
+
type: "string",
|
|
214
|
+
minLength: 1,
|
|
215
|
+
description: "Complete, self-contained instructions for the image generation or edit.",
|
|
216
|
+
},
|
|
217
|
+
transparent_background: { type: "boolean" },
|
|
218
|
+
num_last_images_to_include: { type: "integer", minimum: 0, maximum: 5 },
|
|
219
|
+
},
|
|
220
|
+
required: ["prompt", "transparent_background", "num_last_images_to_include"],
|
|
221
|
+
additionalProperties: false,
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
154
225
|
function requestOptions(req: GenerateRequest): OpenAIResponsesOptions {
|
|
155
226
|
const { outputFormat, ...imageControls } = req.imageGenerationOptions ?? {};
|
|
156
227
|
const auto = req.imageGeneration === "auto";
|
|
228
|
+
|
|
157
229
|
if (auto && req.outputSchema) {
|
|
158
230
|
throw new ImageGenerationError(
|
|
159
231
|
"gate",
|
|
160
232
|
"Automatic image generation does not support structured output or external tools",
|
|
161
233
|
);
|
|
162
234
|
}
|
|
235
|
+
|
|
163
236
|
if (auto && outputFormat && outputFormat !== "png") {
|
|
164
237
|
throw new ImageGenerationError("gate", "Automatic image generation only supports PNG output");
|
|
165
238
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
: {}),
|
|
205
|
-
history: inputItems(req),
|
|
206
|
-
...(asReasoning(req) ? { reasoning: asReasoning(req) } : {}),
|
|
207
|
-
...(req.verbosity ? { verbosity: req.verbosity as OpenAIResponsesOptions["verbosity"] } : {}),
|
|
208
|
-
...(req.serviceTier
|
|
209
|
-
? { serviceTier: req.serviceTier === "fast" ? "priority" : req.serviceTier }
|
|
210
|
-
: {}),
|
|
211
|
-
...(req.promptCacheKey ? { promptCacheKey: req.promptCacheKey } : {}),
|
|
212
|
-
...(req.outputSchema ? { outputSchema: { schema: req.outputSchema as JsonValue } } : {}),
|
|
213
|
-
...(req.imageGeneration && !auto
|
|
214
|
-
? {
|
|
215
|
-
imageGeneration: req.imageGenerationOptions
|
|
216
|
-
? {
|
|
217
|
-
...imageControls,
|
|
218
|
-
...(outputFormat ? { output_format: outputFormat } : {}),
|
|
219
|
-
}
|
|
220
|
-
: true,
|
|
221
|
-
}
|
|
222
|
-
: {}),
|
|
223
|
-
};
|
|
239
|
+
|
|
240
|
+
const options: OpenAIResponsesOptions = { model: req.model ?? "", history: inputItems(req) };
|
|
241
|
+
|
|
242
|
+
if (req.systemPrompt) options.instructions = req.systemPrompt;
|
|
243
|
+
|
|
244
|
+
if (auto) {
|
|
245
|
+
options.instructions = [req.systemPrompt, AUTO_IMAGE_INSTRUCTIONS].filter(Boolean).join("\n\n");
|
|
246
|
+
options.parallelToolCalls = false;
|
|
247
|
+
options.tools = [AUTO_IMAGE_TOOL];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const reasoning = asReasoning(req);
|
|
251
|
+
|
|
252
|
+
if (reasoning) options.reasoning = reasoning;
|
|
253
|
+
|
|
254
|
+
if (req.verbosity) options.verbosity = req.verbosity;
|
|
255
|
+
|
|
256
|
+
if (req.serviceTier) {
|
|
257
|
+
options.serviceTier = req.serviceTier === "fast" ? "priority" : req.serviceTier;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (req.promptCacheKey) options.promptCacheKey = req.promptCacheKey;
|
|
261
|
+
|
|
262
|
+
if (req.outputSchema) options.outputSchema = { schema: req.outputSchema };
|
|
263
|
+
|
|
264
|
+
if (req.imageGeneration && !auto) {
|
|
265
|
+
if (req.imageGenerationOptions) {
|
|
266
|
+
const controls: OpenAIResponseItem = { ...imageControls };
|
|
267
|
+
|
|
268
|
+
if (outputFormat) controls.output_format = outputFormat;
|
|
269
|
+
options.imageGeneration = controls;
|
|
270
|
+
} else {
|
|
271
|
+
options.imageGeneration = true;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return options;
|
|
224
276
|
}
|
|
225
277
|
|
|
226
278
|
// quota와 cache affinity로 토큰을 선택해 HTTPS/WS로 전송한다. 로컬 동시성 제한은 없다.
|
|
@@ -254,18 +306,18 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
254
306
|
|
|
255
307
|
async start(): Promise<void> {
|
|
256
308
|
const { rows } = await TokenModel.findMany("A");
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
309
|
+
|
|
310
|
+
for (const row of rows) {
|
|
311
|
+
if (row.provider !== "openai") continue;
|
|
312
|
+
|
|
313
|
+
if (!isOpenAICredentials(row.credentials)) {
|
|
314
|
+
logger.warn(`skipping token ${row.name}: credentials are not OpenAI OAuth credentials`);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
this.setToken(row.id, row.name, row.credentials, row.quota_threshold, row.weight, row.active);
|
|
319
|
+
}
|
|
320
|
+
|
|
269
321
|
logger.info(`started direct OpenAI runtime with ${this.tokenMetadata.size} tokens`);
|
|
270
322
|
}
|
|
271
323
|
|
|
@@ -311,12 +363,14 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
311
363
|
|
|
312
364
|
onTokenDeactivated(id: number): void {
|
|
313
365
|
const token = this.tokenMetadata.get(id);
|
|
366
|
+
|
|
314
367
|
if (token) token.active = false;
|
|
315
368
|
this.selector.resetScores();
|
|
316
369
|
}
|
|
317
370
|
|
|
318
371
|
onTokenActivated(id: number): void {
|
|
319
372
|
const token = this.tokenMetadata.get(id);
|
|
373
|
+
|
|
320
374
|
if (token) token.active = true;
|
|
321
375
|
this.selector.resetScores();
|
|
322
376
|
}
|
|
@@ -331,7 +385,9 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
331
385
|
}>,
|
|
332
386
|
): Promise<void> {
|
|
333
387
|
const ids = new Set(rows.map((r) => r.id));
|
|
388
|
+
|
|
334
389
|
for (const id of this.tokenMetadata.keys()) if (!ids.has(id)) await this.onTokenRemoved(id);
|
|
390
|
+
|
|
335
391
|
for (const row of rows)
|
|
336
392
|
await this.onTokenUpdated(row.id, row.name, row.credentials, row.quotaThreshold, row.weight);
|
|
337
393
|
}
|
|
@@ -346,10 +402,11 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
346
402
|
"gate",
|
|
347
403
|
"image generation is not supported on the streaming path",
|
|
348
404
|
);
|
|
405
|
+
|
|
349
406
|
try {
|
|
350
407
|
cb.onComplete(await this.run(req, cb.onDelta));
|
|
351
408
|
} catch (error) {
|
|
352
|
-
cb.onError(error
|
|
409
|
+
cb.onError(error instanceof Error ? error : new Error(String(error)));
|
|
353
410
|
throw error;
|
|
354
411
|
}
|
|
355
412
|
}
|
|
@@ -357,26 +414,34 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
357
414
|
async run(req: GenerateRequest, onDelta?: (text: string) => void): Promise<GenerateResult> {
|
|
358
415
|
if (req.thinking === false) {
|
|
359
416
|
const model = req.model ?? "";
|
|
417
|
+
|
|
360
418
|
if (!THINKING_OFF_MODELS.has(model)) {
|
|
361
419
|
const reason =
|
|
362
420
|
model === "gpt-6-astra" || model === "gpt-6.1-sol"
|
|
363
421
|
? "the subscription backend requires thinking"
|
|
364
422
|
: "thinking-off support has not been verified on the subscription backend";
|
|
423
|
+
|
|
365
424
|
throw new ThinkingValidationError(
|
|
366
425
|
`thinking:false is unavailable for OpenAI model ${model || "<default>"}: ${reason}`,
|
|
367
426
|
);
|
|
368
427
|
}
|
|
369
428
|
}
|
|
429
|
+
|
|
370
430
|
const signal = activeSignal(req.abortSignal, req.timeoutMs);
|
|
431
|
+
|
|
371
432
|
if (signal.aborted) throw abortError(signal);
|
|
433
|
+
|
|
372
434
|
const selection = await this.selectToken(
|
|
373
435
|
req.preferredTokenId,
|
|
374
436
|
req.requirePreferredToken ?? false,
|
|
375
437
|
signal,
|
|
376
438
|
);
|
|
439
|
+
|
|
377
440
|
this.inFlight++;
|
|
441
|
+
|
|
378
442
|
try {
|
|
379
443
|
if (signal.aborted) throw abortError(signal);
|
|
444
|
+
|
|
380
445
|
return await this.runDirect(selection, { ...req, abortSignal: signal }, onDelta);
|
|
381
446
|
} finally {
|
|
382
447
|
this.inFlight--;
|
|
@@ -394,6 +459,7 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
394
459
|
let outputMessageAccepted = false;
|
|
395
460
|
const messagePhases = new Map<string, string>();
|
|
396
461
|
const streamedMessageIds = new Set<string>();
|
|
462
|
+
|
|
397
463
|
let usage = {
|
|
398
464
|
totalTokens: 0,
|
|
399
465
|
inputTokens: 0,
|
|
@@ -401,48 +467,67 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
401
467
|
cachedInputTokens: 0,
|
|
402
468
|
reasoningOutputTokens: 0,
|
|
403
469
|
};
|
|
470
|
+
|
|
404
471
|
const images: GeneratedImage[] = [];
|
|
405
472
|
let imageAttempted = false;
|
|
406
473
|
const auto = req.imageGeneration === "auto";
|
|
407
474
|
const imageCalls = new Map<string, ImageToolArguments>();
|
|
408
|
-
const imageCallIds = new Set<
|
|
475
|
+
const imageCallIds = new Set<JsonValue | undefined>();
|
|
409
476
|
let imageToolAttempted = false;
|
|
410
477
|
let driverCompleted = false;
|
|
411
478
|
let servingModel = req.model ?? "";
|
|
412
479
|
const metadata = selection.metadata;
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
480
|
+
|
|
481
|
+
const driverResult = (): GenerateResult => {
|
|
482
|
+
const result: GenerateResult = {
|
|
483
|
+
text,
|
|
484
|
+
tokenName: metadata.name,
|
|
485
|
+
usage,
|
|
486
|
+
durationMs: Date.now() - startedAt,
|
|
487
|
+
ttftMs: firstDeltaAt === undefined ? null : firstDeltaAt - startedAt,
|
|
488
|
+
model: servingModel,
|
|
489
|
+
threadCoord: {
|
|
490
|
+
workerId: selection.tokenId,
|
|
491
|
+
threadId: req.promptCacheKey ?? "",
|
|
492
|
+
epoch: -1,
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
if (servingModel !== req.model) result.requestedModel = req.model;
|
|
497
|
+
|
|
498
|
+
return result;
|
|
499
|
+
};
|
|
500
|
+
|
|
423
501
|
const clientEntry = this.acquireClient(selection);
|
|
502
|
+
|
|
424
503
|
try {
|
|
425
504
|
for await (const event of clientEntry.client.responses(
|
|
426
505
|
requestOptions(req),
|
|
427
506
|
req.abortSignal,
|
|
428
507
|
)) {
|
|
508
|
+
const itemId = event.type === "output-item" ? jsonString(event.item.id) : undefined;
|
|
509
|
+
const itemPhase = event.type === "output-item" ? jsonString(event.item.phase) : undefined;
|
|
510
|
+
|
|
429
511
|
if (
|
|
430
512
|
req.stopAfterOutputMessage &&
|
|
431
513
|
event.type === "output-item" &&
|
|
432
514
|
event.item.type === "message" &&
|
|
433
|
-
|
|
434
|
-
|
|
515
|
+
itemId !== undefined &&
|
|
516
|
+
itemPhase !== undefined
|
|
435
517
|
) {
|
|
436
|
-
messagePhases.set(
|
|
518
|
+
messagePhases.set(itemId, itemPhase);
|
|
437
519
|
}
|
|
520
|
+
|
|
438
521
|
if (event.type === "text-delta") {
|
|
439
522
|
if (outputMessageAccepted) continue;
|
|
523
|
+
|
|
440
524
|
if (req.stopAfterOutputMessage && event.itemId) {
|
|
441
525
|
// Buffer commentary/unclassified messages until their completed item.
|
|
442
526
|
// Known final answers retain incremental streaming.
|
|
443
527
|
if (messagePhases.get(event.itemId) !== "final_answer") continue;
|
|
444
528
|
streamedMessageIds.add(event.itemId);
|
|
445
529
|
}
|
|
530
|
+
|
|
446
531
|
firstDeltaAt ??= Date.now();
|
|
447
532
|
text += event.text;
|
|
448
533
|
onDelta?.(event.text);
|
|
@@ -455,34 +540,38 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
455
540
|
Array.isArray(event.item.content)
|
|
456
541
|
) {
|
|
457
542
|
const messageText = event.item.content
|
|
458
|
-
.
|
|
459
|
-
|
|
543
|
+
.flatMap((part) => {
|
|
544
|
+
const partText =
|
|
545
|
+
isJsonObject(part) && part.type === "output_text"
|
|
546
|
+
? jsonString(part.text)
|
|
547
|
+
: undefined;
|
|
548
|
+
|
|
549
|
+
return partText === undefined ? [] : [partText];
|
|
550
|
+
})
|
|
460
551
|
.join("");
|
|
552
|
+
|
|
461
553
|
// A valid client tool request ends this application turn. Later answers cannot
|
|
462
554
|
// know its result yet. Never split/repair malformed JSON within a message.
|
|
463
|
-
const phase =
|
|
555
|
+
const phase = itemPhase;
|
|
556
|
+
|
|
464
557
|
if (messageText) outputMessageAccepted = req.stopAfterOutputMessage(messageText, phase);
|
|
558
|
+
|
|
465
559
|
if (
|
|
466
|
-
|
|
467
|
-
!streamedMessageIds.has(
|
|
560
|
+
itemId !== undefined &&
|
|
561
|
+
!streamedMessageIds.has(itemId) &&
|
|
468
562
|
(phase !== "commentary" || outputMessageAccepted) &&
|
|
469
563
|
messageText
|
|
470
564
|
) {
|
|
471
565
|
firstDeltaAt ??= Date.now();
|
|
472
566
|
text += messageText;
|
|
473
567
|
onDelta?.(messageText);
|
|
474
|
-
streamedMessageIds.add(
|
|
568
|
+
streamedMessageIds.add(itemId);
|
|
475
569
|
}
|
|
476
570
|
} else if (event.type === "image") {
|
|
477
571
|
if (auto)
|
|
478
572
|
throw new ImageGenerationError("incomplete", "Unexpected hosted image generation");
|
|
479
573
|
imageAttempted = true;
|
|
480
|
-
images.push(
|
|
481
|
-
data: event.base64,
|
|
482
|
-
mediaType: event.mimeType,
|
|
483
|
-
revisedPrompt: event.revisedPrompt ?? null,
|
|
484
|
-
...(event.generation ? { generation: event.generation } : {}),
|
|
485
|
-
});
|
|
574
|
+
images.push(generatedImageFrom(event));
|
|
486
575
|
} else if (event.type === "output-item" && event.item.type === "image_generation_call") {
|
|
487
576
|
if (auto)
|
|
488
577
|
throw new ImageGenerationError("incomplete", "Unexpected hosted image generation");
|
|
@@ -491,37 +580,33 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
491
580
|
if (event.item.name !== IMAGE_TOOL) {
|
|
492
581
|
throw new ImageGenerationError("incomplete", "Unexpected automatic image tool call");
|
|
493
582
|
}
|
|
583
|
+
|
|
494
584
|
imageToolAttempted = true;
|
|
495
585
|
imageCallIds.add(event.item.call_id);
|
|
586
|
+
|
|
496
587
|
if (!event.completed) continue;
|
|
497
|
-
const id = event.item.call_id;
|
|
498
|
-
|
|
588
|
+
const id = jsonString(event.item.call_id);
|
|
589
|
+
const rawArguments = jsonString(event.item.arguments);
|
|
590
|
+
let decoded: JsonValue;
|
|
591
|
+
|
|
499
592
|
try {
|
|
500
|
-
|
|
593
|
+
if (rawArguments === undefined) throw new Error("missing arguments");
|
|
594
|
+
// JSON.parse without a reviver yields only JSON values.
|
|
595
|
+
decoded = JSON.parse(rawArguments);
|
|
501
596
|
} catch {
|
|
502
597
|
throw new ImageGenerationError("incomplete", "Invalid image tool arguments");
|
|
503
598
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
typeof args !== "object" ||
|
|
509
|
-
Array.isArray(args) ||
|
|
510
|
-
Object.keys(args).length !== 3 ||
|
|
511
|
-
!("prompt" in args) ||
|
|
512
|
-
typeof args.prompt !== "string" ||
|
|
513
|
-
!args.prompt.trim() ||
|
|
514
|
-
!("transparent_background" in args) ||
|
|
515
|
-
typeof args.transparent_background !== "boolean" ||
|
|
516
|
-
!("num_last_images_to_include" in args) ||
|
|
517
|
-
typeof args.num_last_images_to_include !== "number" ||
|
|
518
|
-
!Number.isInteger(args.num_last_images_to_include) ||
|
|
519
|
-
args.num_last_images_to_include < 0 ||
|
|
520
|
-
args.num_last_images_to_include > 5
|
|
521
|
-
) {
|
|
599
|
+
|
|
600
|
+
const parsedArgs = ImageToolArgumentsSchema.safeParse(decoded);
|
|
601
|
+
|
|
602
|
+
if (!id || !parsedArgs.success) {
|
|
522
603
|
throw new ImageGenerationError("incomplete", "Invalid image tool arguments");
|
|
523
604
|
}
|
|
605
|
+
|
|
606
|
+
const args = parsedArgs.data;
|
|
607
|
+
|
|
524
608
|
const previous = imageCalls.get(id);
|
|
609
|
+
|
|
525
610
|
if (
|
|
526
611
|
previous &&
|
|
527
612
|
(previous.prompt !== args.prompt ||
|
|
@@ -530,6 +615,7 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
530
615
|
) {
|
|
531
616
|
throw new ImageGenerationError("incomplete", "Conflicting image tool calls");
|
|
532
617
|
}
|
|
618
|
+
|
|
533
619
|
imageCalls.set(id, {
|
|
534
620
|
prompt: args.prompt,
|
|
535
621
|
transparent_background: args.transparent_background,
|
|
@@ -537,6 +623,7 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
537
623
|
});
|
|
538
624
|
} else if (event.type === "completed") {
|
|
539
625
|
const imageUsage = event.imageGeneration?.usage;
|
|
626
|
+
|
|
540
627
|
// The backend also reports all-zero image usage for text-only responses.
|
|
541
628
|
if (
|
|
542
629
|
auto &&
|
|
@@ -548,6 +635,7 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
548
635
|
throw new ImageGenerationError("incomplete", "Unexpected hosted image generation");
|
|
549
636
|
driverCompleted = true;
|
|
550
637
|
servingModel = event.model ?? servingModel;
|
|
638
|
+
|
|
551
639
|
if (event.usage) {
|
|
552
640
|
usage = {
|
|
553
641
|
totalTokens: event.usage.totalTokens,
|
|
@@ -557,20 +645,29 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
557
645
|
reasoningOutputTokens: event.usage.reasoningTokens,
|
|
558
646
|
};
|
|
559
647
|
}
|
|
648
|
+
|
|
560
649
|
if (event.imageGeneration) {
|
|
561
650
|
const hostedImages = images.filter(
|
|
562
651
|
(image) => image.generation?.route === "codex-responses",
|
|
563
652
|
);
|
|
653
|
+
|
|
564
654
|
for (const [index, image] of hostedImages.entries()) {
|
|
565
|
-
|
|
655
|
+
const generation: ImageGenerationMetadata = {
|
|
566
656
|
route: "codex-responses",
|
|
567
|
-
...(event.imageGeneration.model ? { model: event.imageGeneration.model } : {}),
|
|
568
657
|
...image.generation,
|
|
569
|
-
// Tool usage covers the whole response, not each returned image.
|
|
570
|
-
...(index === 0 && event.imageGeneration.usage
|
|
571
|
-
? { usage: event.imageGeneration.usage }
|
|
572
|
-
: {}),
|
|
573
658
|
};
|
|
659
|
+
|
|
660
|
+
// The image's own model wins; the response-level model only fills a gap.
|
|
661
|
+
if (generation.model === undefined && event.imageGeneration.model) {
|
|
662
|
+
generation.model = event.imageGeneration.model;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Tool usage covers the whole response, not each returned image.
|
|
666
|
+
if (index === 0 && event.imageGeneration.usage) {
|
|
667
|
+
generation.usage = event.imageGeneration.usage;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
image.generation = generation;
|
|
574
671
|
}
|
|
575
672
|
}
|
|
576
673
|
} else if (event.type === "error") {
|
|
@@ -579,21 +676,26 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
579
676
|
throw event.error;
|
|
580
677
|
}
|
|
581
678
|
}
|
|
679
|
+
|
|
582
680
|
if (auto && imageToolAttempted) {
|
|
583
681
|
if (!driverCompleted || imageCalls.size !== 1 || imageCallIds.size !== 1) {
|
|
584
682
|
throw new ImageGenerationError("incomplete", "Expected one completed image tool call");
|
|
585
683
|
}
|
|
684
|
+
|
|
586
685
|
if (req.abortSignal?.aborted) throw abortError(req.abortSignal);
|
|
587
686
|
const args = [...imageCalls.values()][0]!;
|
|
588
687
|
const referenceCount = args.num_last_images_to_include;
|
|
589
688
|
const references = referenceCount ? imageReferences(req) : [];
|
|
689
|
+
|
|
590
690
|
if (referenceCount > references.length) {
|
|
591
691
|
throw new ImageGenerationError(
|
|
592
692
|
"incomplete",
|
|
593
693
|
"Requested image references are unavailable",
|
|
594
694
|
);
|
|
595
695
|
}
|
|
696
|
+
|
|
596
697
|
let imageCompleted = false;
|
|
698
|
+
|
|
597
699
|
for await (const event of clientEntry.client.responses(
|
|
598
700
|
{
|
|
599
701
|
model: "gpt-image-2",
|
|
@@ -616,26 +718,25 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
616
718
|
},
|
|
617
719
|
req.abortSignal,
|
|
618
720
|
)) {
|
|
619
|
-
if (event.type === "image")
|
|
620
|
-
images.push({
|
|
621
|
-
data: event.base64,
|
|
622
|
-
mediaType: event.mimeType,
|
|
623
|
-
revisedPrompt: event.revisedPrompt ?? null,
|
|
624
|
-
...(event.generation ? { generation: event.generation } : {}),
|
|
625
|
-
});
|
|
721
|
+
if (event.type === "image") images.push(generatedImageFrom(event));
|
|
626
722
|
else if (event.type === "completed") imageCompleted = true;
|
|
627
723
|
else if (event.type === "error")
|
|
628
724
|
throw new ImageGenerationError("incomplete", event.error.message, {
|
|
629
725
|
cause: event.error,
|
|
630
726
|
});
|
|
631
727
|
}
|
|
728
|
+
|
|
632
729
|
imageAttempted = true;
|
|
730
|
+
|
|
633
731
|
if (!imageCompleted)
|
|
634
732
|
throw new ImageGenerationError("incomplete", "Image generation did not complete");
|
|
635
733
|
}
|
|
734
|
+
|
|
636
735
|
if (auto && imageAttempted && !driverCompleted)
|
|
637
736
|
throw new ImageGenerationError("incomplete", "Image generation response did not complete");
|
|
737
|
+
|
|
638
738
|
if (req.abortSignal?.aborted) throw abortError(req.abortSignal);
|
|
739
|
+
|
|
639
740
|
if ((req.imageGeneration === true || (auto && imageAttempted)) && images.length === 0) {
|
|
640
741
|
throw new ImageGenerationError(
|
|
641
742
|
imageAttempted ? "incomplete" : "not_called",
|
|
@@ -654,29 +755,35 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
654
755
|
error instanceof Error ? error.message : String(error),
|
|
655
756
|
{ cause: error },
|
|
656
757
|
);
|
|
758
|
+
|
|
657
759
|
if (error instanceof Error) failure.name = error.name;
|
|
658
760
|
// Only the completed chat stage is chargeable here; discard partial image output.
|
|
659
761
|
failure.completedDriver = driverResult();
|
|
660
762
|
throw failure;
|
|
661
763
|
}
|
|
764
|
+
|
|
662
765
|
throw error;
|
|
663
766
|
} finally {
|
|
664
767
|
this.releaseClient(clientEntry);
|
|
665
768
|
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
769
|
+
|
|
770
|
+
const result = driverResult();
|
|
771
|
+
|
|
772
|
+
if (images.length) result.images = images;
|
|
773
|
+
|
|
774
|
+
return result;
|
|
670
775
|
}
|
|
671
776
|
|
|
672
777
|
/** 세대가 바뀐 client 는 사용 중인 요청이 끝난 뒤에 닫는다. */
|
|
673
778
|
acquireClient(selection: TokenSelection): ClientEntry {
|
|
674
779
|
const metadata = selection.metadata;
|
|
675
780
|
let entry = this.clients.get(selection.tokenId);
|
|
781
|
+
|
|
676
782
|
if (entry && entry.generation !== metadata.generation) {
|
|
677
783
|
this.retireClient(selection.tokenId);
|
|
678
784
|
entry = undefined;
|
|
679
785
|
}
|
|
786
|
+
|
|
680
787
|
if (!entry) {
|
|
681
788
|
const client = this.clientFactory(
|
|
682
789
|
{
|
|
@@ -690,15 +797,19 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
690
797
|
},
|
|
691
798
|
selection.tokenId,
|
|
692
799
|
);
|
|
800
|
+
|
|
693
801
|
entry = { generation: metadata.generation, client, inFlight: 0, retired: false };
|
|
694
802
|
this.clients.set(selection.tokenId, entry);
|
|
695
803
|
}
|
|
804
|
+
|
|
696
805
|
entry.inFlight++;
|
|
806
|
+
|
|
697
807
|
return entry;
|
|
698
808
|
}
|
|
699
809
|
|
|
700
810
|
releaseClient(entry: ClientEntry): void {
|
|
701
811
|
entry.inFlight = Math.max(0, entry.inFlight - 1);
|
|
812
|
+
|
|
702
813
|
if (entry.retired && entry.inFlight === 0) {
|
|
703
814
|
this.retiredClients.delete(entry);
|
|
704
815
|
entry.client.close?.();
|
|
@@ -707,9 +818,11 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
707
818
|
|
|
708
819
|
retireClient(tokenId: number): void {
|
|
709
820
|
const entry = this.clients.get(tokenId);
|
|
821
|
+
|
|
710
822
|
if (!entry) return;
|
|
711
823
|
this.clients.delete(tokenId);
|
|
712
824
|
entry.retired = true;
|
|
825
|
+
|
|
713
826
|
if (entry.inFlight === 0) entry.client.close?.();
|
|
714
827
|
else this.retiredClients.add(entry);
|
|
715
828
|
}
|
|
@@ -722,14 +835,18 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
722
835
|
): Promise<TokenSelection> {
|
|
723
836
|
if (preferredTokenId !== undefined) {
|
|
724
837
|
const preferred = this.tokenMetadata.get(preferredTokenId);
|
|
838
|
+
|
|
725
839
|
if (preferred?.active && (await this.isQuotaEligible(preferredTokenId, preferred, signal))) {
|
|
726
840
|
return { tokenId: preferredTokenId, metadata: preferred };
|
|
727
841
|
}
|
|
842
|
+
|
|
728
843
|
if (requirePreferredToken) {
|
|
729
844
|
if (!preferred?.active) {
|
|
730
845
|
throw new Error(`Preferred openai token ${preferredTokenId} is not available`);
|
|
731
846
|
}
|
|
847
|
+
|
|
732
848
|
const threshold = preferred.quotaThreshold;
|
|
849
|
+
|
|
733
850
|
if (threshold !== null && threshold !== undefined) {
|
|
734
851
|
throw new QuotaThresholdExceededError(
|
|
735
852
|
quotaMessage([{ name: preferred.name, threshold }]),
|
|
@@ -739,23 +856,27 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
739
856
|
}
|
|
740
857
|
|
|
741
858
|
// 콜드 캐시에서 토큰 수만큼 조회 지연이 쌓이지 않도록 병렬 판정한다.
|
|
859
|
+
const activeTokens = [...this.tokenMetadata.entries()].filter(([, token]) => token.active);
|
|
860
|
+
|
|
742
861
|
const checks = await Promise.all(
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
eligible: await this.isQuotaEligible(id, token, signal),
|
|
749
|
-
})),
|
|
862
|
+
activeTokens.map(async ([id, token]) => ({
|
|
863
|
+
id,
|
|
864
|
+
token,
|
|
865
|
+
eligible: await this.isQuotaEligible(id, token, signal),
|
|
866
|
+
})),
|
|
750
867
|
);
|
|
868
|
+
|
|
751
869
|
const eligible = new Set(checks.filter((c) => c.eligible).map((c) => c.id));
|
|
870
|
+
|
|
752
871
|
if (eligible.size === 0) {
|
|
753
|
-
const over = checks
|
|
754
|
-
.
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
872
|
+
const over = checks.flatMap((c) => {
|
|
873
|
+
const threshold = c.token.quotaThreshold;
|
|
874
|
+
|
|
875
|
+
return c.eligible || threshold === null || threshold === undefined
|
|
876
|
+
? []
|
|
877
|
+
: [{ name: c.token.name, threshold }];
|
|
878
|
+
});
|
|
879
|
+
|
|
759
880
|
if (over.length) {
|
|
760
881
|
logger.warn("quota_threshold gate: all_exceeded", {
|
|
761
882
|
provider: "openai",
|
|
@@ -763,11 +884,15 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
763
884
|
});
|
|
764
885
|
throw new QuotaThresholdExceededError(quotaMessage(over));
|
|
765
886
|
}
|
|
887
|
+
|
|
766
888
|
throw new Error("NO_OPENAI_WORKERS");
|
|
767
889
|
}
|
|
890
|
+
|
|
768
891
|
const selected = this.selector.select(eligible);
|
|
769
892
|
const metadata = selected !== null ? this.tokenMetadata.get(selected) : undefined;
|
|
893
|
+
|
|
770
894
|
if (selected === null || !metadata) throw new Error("NO_OPENAI_WORKERS");
|
|
895
|
+
|
|
771
896
|
return { tokenId: selected, metadata };
|
|
772
897
|
}
|
|
773
898
|
|
|
@@ -780,6 +905,7 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
780
905
|
active: boolean,
|
|
781
906
|
): void {
|
|
782
907
|
const old = this.tokenMetadata.get(id);
|
|
908
|
+
|
|
783
909
|
if (old) {
|
|
784
910
|
Object.assign(old, {
|
|
785
911
|
name,
|
|
@@ -799,6 +925,7 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
799
925
|
generation: 1,
|
|
800
926
|
});
|
|
801
927
|
}
|
|
928
|
+
|
|
802
929
|
this.selector.setToken(id, weight);
|
|
803
930
|
this.invalidateRateLimitsCache(id);
|
|
804
931
|
this.quotaBlocked.delete(id);
|
|
@@ -809,9 +936,11 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
809
936
|
signal?: AbortSignal,
|
|
810
937
|
): Promise<OpenAIRateLimitsWithMeta> {
|
|
811
938
|
const token = this.tokenMetadata.get(tokenId);
|
|
939
|
+
|
|
812
940
|
if (!token) throw new Error(`openai token ${tokenId} not found`);
|
|
813
941
|
const generation = token.generation;
|
|
814
942
|
const cached = this.rateLimitsCache.get(tokenId);
|
|
943
|
+
|
|
815
944
|
if (
|
|
816
945
|
cached &&
|
|
817
946
|
cached.generation === token.generation &&
|
|
@@ -821,27 +950,35 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
821
950
|
|
|
822
951
|
// TTL 만료 시 조회를 공유한다. 최초 호출자의 abort로 실패해도 isQuotaEligible은 fail-open한다.
|
|
823
952
|
const pending = this.pendingRateLimits.get(tokenId);
|
|
953
|
+
|
|
824
954
|
if (pending?.token === token && pending.generation === generation) return pending.promise;
|
|
955
|
+
|
|
825
956
|
const fetchPromise = (async () => {
|
|
826
957
|
const result = await readOpenAIQuotaUsage({
|
|
827
958
|
credentials: token.credentials,
|
|
828
959
|
refreshCredentials: () => this.refreshCredentials(tokenId),
|
|
829
960
|
fetch: this.fetchImpl,
|
|
830
|
-
|
|
961
|
+
signal,
|
|
831
962
|
});
|
|
963
|
+
|
|
832
964
|
if (result.kind === "lookup_failed") throw new Error(result.reason);
|
|
965
|
+
|
|
833
966
|
if (!result.raw) throw new Error("OpenAI quota response missing raw rate limits");
|
|
834
967
|
const entry = { data: result.raw, cachedAt: Date.now(), generation };
|
|
968
|
+
|
|
835
969
|
if (this.tokenMetadata.get(tokenId) === token && token.generation === generation) {
|
|
836
970
|
this.rateLimitsCache.set(tokenId, entry);
|
|
837
971
|
}
|
|
972
|
+
|
|
838
973
|
return entry;
|
|
839
974
|
})().finally(() => {
|
|
840
975
|
if (this.pendingRateLimits.get(tokenId)?.promise === fetchPromise) {
|
|
841
976
|
this.pendingRateLimits.delete(tokenId);
|
|
842
977
|
}
|
|
843
978
|
});
|
|
979
|
+
|
|
844
980
|
this.pendingRateLimits.set(tokenId, { token, generation, promise: fetchPromise });
|
|
981
|
+
|
|
845
982
|
return fetchPromise;
|
|
846
983
|
}
|
|
847
984
|
|
|
@@ -849,12 +986,16 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
849
986
|
const previous = this.tokenMetadata.get(tokenId);
|
|
850
987
|
const generation = previous?.generation;
|
|
851
988
|
const refreshed = await handleChatgptAuthTokensRefresh(tokenId);
|
|
989
|
+
|
|
852
990
|
const credentials = {
|
|
853
991
|
accessToken: refreshed.accessToken,
|
|
854
992
|
accountId: refreshed.chatgptAccountId,
|
|
855
993
|
};
|
|
994
|
+
|
|
856
995
|
const current = this.tokenMetadata.get(tokenId);
|
|
996
|
+
|
|
857
997
|
if (!current?.active) throw new Error(`openai token ${tokenId} is no longer active`);
|
|
998
|
+
|
|
858
999
|
// A subscriber update may already contain newer credentials. Never overwrite it
|
|
859
1000
|
// with the result of a refresh that started against an older generation.
|
|
860
1001
|
if (current === previous && current.generation === generation) {
|
|
@@ -862,6 +1003,7 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
862
1003
|
this.retireClient(tokenId);
|
|
863
1004
|
this.invalidateRateLimitsCache(tokenId);
|
|
864
1005
|
}
|
|
1006
|
+
|
|
865
1007
|
return {
|
|
866
1008
|
accessToken: current.credentials.accessToken,
|
|
867
1009
|
accountId: current.credentials.accountId,
|
|
@@ -872,7 +1014,9 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
872
1014
|
if (token.quotaThreshold === null || token.quotaThreshold === undefined) return true;
|
|
873
1015
|
const generation = token.generation;
|
|
874
1016
|
const result = await readOpenAIQuotaUsage(async () => this.getRateLimitsByTokenId(id, signal));
|
|
1017
|
+
|
|
875
1018
|
if (this.tokenMetadata.get(id) !== token || token.generation !== generation) return false;
|
|
1019
|
+
|
|
876
1020
|
if (result.kind === "lookup_failed") {
|
|
877
1021
|
this.quotaBlocked.delete(id);
|
|
878
1022
|
logger.warn("quota_threshold gate: lookup_fail_open", {
|
|
@@ -880,20 +1024,25 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
880
1024
|
tokenName: token.name,
|
|
881
1025
|
lookupReason: result.reason,
|
|
882
1026
|
});
|
|
1027
|
+
|
|
883
1028
|
return true;
|
|
884
1029
|
}
|
|
1030
|
+
|
|
885
1031
|
if (result.utilizationPct >= token.quotaThreshold) {
|
|
886
1032
|
if (!this.quotaBlocked.has(id))
|
|
887
1033
|
logger.info(
|
|
888
1034
|
`quota_threshold gate: over_threshold ${token.name}[${id}] (${result.utilizationPct}% >= ${token.quotaThreshold}%)`,
|
|
889
1035
|
);
|
|
890
1036
|
this.quotaBlocked.add(id);
|
|
1037
|
+
|
|
891
1038
|
return false;
|
|
892
1039
|
}
|
|
1040
|
+
|
|
893
1041
|
if (this.quotaBlocked.delete(id))
|
|
894
1042
|
logger.info(
|
|
895
1043
|
`quota_threshold gate: recovered ${token.name}[${id}] (${result.utilizationPct}% < ${token.quotaThreshold}%)`,
|
|
896
1044
|
);
|
|
1045
|
+
|
|
897
1046
|
return true;
|
|
898
1047
|
}
|
|
899
1048
|
|
|
@@ -914,14 +1063,18 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
914
1063
|
resetsAt: number | null;
|
|
915
1064
|
}> {
|
|
916
1065
|
const now = Date.now();
|
|
1066
|
+
|
|
917
1067
|
return [...this.tokenMetadata.entries()]
|
|
918
1068
|
.map(([id, t]) => {
|
|
919
1069
|
const cached = this.rateLimitsCache.get(id);
|
|
1070
|
+
|
|
920
1071
|
const fresh =
|
|
921
1072
|
cached !== undefined &&
|
|
922
1073
|
cached.generation === t.generation &&
|
|
923
1074
|
now - cached.cachedAt < OpenAIDispatcher.RATE_LIMITS_CACHE_TTL;
|
|
1075
|
+
|
|
924
1076
|
const primary = fresh ? cached.data.rateLimits?.primary : undefined;
|
|
1077
|
+
|
|
925
1078
|
return {
|
|
926
1079
|
name: t.name,
|
|
927
1080
|
usedPercent: primary?.usedPercent ?? null,
|