pyric 0.1.0-alpha.11 → 0.1.0-alpha.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md.orig +0 -1
- package/dist/{firestore-values/index.d.ts → firestore/internal/value-codec.d.ts} +2 -2
- package/dist/firestore/internal/value-codec.d.ts.map +1 -0
- package/dist/{firestore-values/index.js → firestore/internal/value-codec.js} +10 -10
- package/dist/firestore/internal/value-codec.js.map +1 -0
- package/dist/sandbox/admin-firestore/remote/value-codec.d.ts +2 -1
- package/dist/sandbox/admin-firestore/remote/value-codec.d.ts.map +1 -1
- package/dist/sandbox/admin-firestore/remote/value-codec.js +3 -2
- package/dist/sandbox/admin-firestore/remote/value-codec.js.map +1 -1
- package/dist/sandbox/persistence/chunk-format.js +1 -1
- package/dist/sandbox/persistence/chunk-format.js.map +1 -1
- package/dist/sandbox/persistence/serialize.d.ts +1 -1
- package/dist/sandbox/persistence/serialize.d.ts.map +1 -1
- package/dist/sandbox/persistence/serialize.js +6 -6
- package/dist/sandbox/persistence/serialize.js.map +1 -1
- package/package.json +6 -5
- package/src/ai/backend.ts +49 -0
- package/src/ai/broker/broker.ts +254 -0
- package/src/ai/broker/index.ts +59 -0
- package/src/ai/broker/openai-engine.ts +646 -0
- package/src/ai/broker/scripted-engine.ts +277 -0
- package/src/ai/broker/synthesizer.ts +340 -0
- package/src/ai/broker/types.ts +194 -0
- package/src/ai/enums.ts +193 -0
- package/src/ai/errors.ts +125 -0
- package/src/ai/index.ts +139 -0
- package/src/ai/instances.ts +136 -0
- package/src/ai/internal.ts +57 -0
- package/src/ai/models.ts +344 -0
- package/src/ai/request-helpers.ts +214 -0
- package/src/ai/response-helpers.ts +230 -0
- package/src/ai/sandbox-plane.ts +219 -0
- package/src/ai/schema.ts +247 -0
- package/src/ai/scripting.ts +114 -0
- package/src/ai/target.ts +36 -0
- package/src/ai/types.ts +491 -0
- package/src/app/diagnostics.test.ts +44 -0
- package/src/app/diagnostics.ts +105 -0
- package/src/app/dispatch.test.ts +142 -0
- package/src/app/index.ts +15 -0
- package/src/app/internal.ts +34 -0
- package/src/app/options.ts +18 -0
- package/src/app/package-resolution.test.ts +14 -0
- package/src/app/register.test.ts +31 -0
- package/src/app/register.ts +45 -0
- package/src/app/registry.ts +193 -0
- package/src/app/runtime.ts +148 -0
- package/src/app/types.ts +22 -0
- package/src/auth/action-code-url.ts +133 -0
- package/src/auth/action-codes.ts +465 -0
- package/src/auth/auth-errors.ts +24 -0
- package/src/auth/config-tokens.ts +101 -0
- package/src/auth/credentials.ts +228 -0
- package/src/auth/email-link.ts +207 -0
- package/src/auth/enums.ts +214 -0
- package/src/auth/index.ts +2 -0
- package/src/auth/instances.ts +127 -0
- package/src/auth/linking.ts +224 -0
- package/src/auth/modular.ts +118 -0
- package/src/auth/password-policy.ts +102 -0
- package/src/auth/providers.ts +250 -0
- package/src/auth/reauth.ts +194 -0
- package/src/auth/sandbox/driver.ts +142 -0
- package/src/auth/sandbox/persistence.ts +156 -0
- package/src/auth/sandbox/project-store.ts +31 -0
- package/src/auth/sandbox-auth-flow.ts +255 -0
- package/src/auth/sandbox-backend-types.ts +200 -0
- package/src/auth/sandbox-backend.ts +2259 -0
- package/src/auth/sandbox-credential-validators.ts +52 -0
- package/src/auth/sandbox-token.ts +32 -0
- package/src/auth/sign-in.ts +194 -0
- package/src/auth/target.ts +43 -0
- package/src/auth/tokens.ts +155 -0
- package/src/auth/types.ts +298 -0
- package/src/auth/user-lifecycle.ts +93 -0
- package/src/database/index.ts +8 -0
- package/src/database/listener-registry.ts +89 -0
- package/src/database/modular.ts +1483 -0
- package/src/database/replay.ts +213 -0
- package/src/database/sandbox/backend-for.ts +36 -0
- package/src/database/sandbox/backend.ts +1805 -0
- package/src/database/sandbox/data-tree.ts +322 -0
- package/src/database/sandbox/normalize.ts +207 -0
- package/src/database/sandbox/push-id.ts +86 -0
- package/src/database/sandbox/query.ts +383 -0
- package/src/database/sandbox/rules-eval.ts +202 -0
- package/src/database/sandbox/sentinels.ts +114 -0
- package/src/database/sandbox-controls.ts +33 -0
- package/src/firestore/aggregates.ts +104 -0
- package/src/firestore/equality.ts +32 -0
- package/src/firestore/field-values.ts +203 -0
- package/src/firestore/index.ts +60 -0
- package/src/firestore/instances.ts +292 -0
- package/src/firestore/internal/index.ts +14 -0
- package/src/firestore/internal/listener-routing.ts +3 -0
- package/src/firestore/internal/value-codec.ts +175 -0
- package/src/firestore/listeners.ts +280 -0
- package/src/firestore/persistence.ts +381 -0
- package/src/firestore/query-constraints.ts +283 -0
- package/src/firestore/reads.ts +67 -0
- package/src/firestore/refs.ts +190 -0
- package/src/firestore/sandbox/activity-bounded.ts +12 -0
- package/src/firestore/sandbox/activity-contract.ts +60 -0
- package/src/firestore/sandbox/activity-listener-retention.ts +21 -0
- package/src/firestore/sandbox/activity-monitor.ts +547 -0
- package/src/firestore/sandbox/activity-public-identity.ts +90 -0
- package/src/firestore/sandbox/activity-query-value.ts +60 -0
- package/src/firestore/sandbox/activity-structural-identity.ts +11 -0
- package/src/firestore/sandbox/activity-value-registry.ts +126 -0
- package/src/firestore/sandbox/admin-compat/batch.ts +86 -0
- package/src/firestore/sandbox/admin-compat/doc-ref.ts +184 -0
- package/src/firestore/sandbox/admin-compat/firestore.ts +154 -0
- package/src/firestore/sandbox/admin-compat/index.ts +96 -0
- package/src/firestore/sandbox/admin-compat/paths.ts +53 -0
- package/src/firestore/sandbox/admin-compat/query.ts +983 -0
- package/src/firestore/sandbox/admin-compat/read-translation.ts +66 -0
- package/src/firestore/sandbox/admin-compat/snapshots.ts +60 -0
- package/src/firestore/sandbox/admin-compat/transaction.ts +102 -0
- package/src/firestore/sandbox/admin-compat/types.ts +404 -0
- package/src/firestore/sandbox/admin-compat/value-order.ts +186 -0
- package/src/firestore/sandbox/auto-id.ts +40 -0
- package/src/firestore/sandbox/converters/bytes-geopoint.ts +100 -0
- package/src/firestore/sandbox/converters/fieldvalue.ts +172 -0
- package/src/firestore/sandbox/converters/reference.ts +59 -0
- package/src/firestore/sandbox/converters/timestamp.ts +65 -0
- package/src/firestore/sandbox/converters/user-timestamp.ts +72 -0
- package/src/firestore/sandbox/converters/vector.ts +51 -0
- package/src/firestore/sandbox/errors.ts +137 -0
- package/src/firestore/sandbox/event-bus.ts +117 -0
- package/src/firestore/sandbox/event-log.ts +134 -0
- package/src/firestore/sandbox/field-merge.ts +245 -0
- package/src/firestore/sandbox/history-controls.ts +98 -0
- package/src/firestore/sandbox/list-query-proof.ts +221 -0
- package/src/firestore/sandbox/listener-delivery.ts +38 -0
- package/src/firestore/sandbox/listener-dispatch.ts +914 -0
- package/src/firestore/sandbox/local-environment.ts +2012 -0
- package/src/firestore/sandbox/local-state.ts +538 -0
- package/src/firestore/sandbox/overlay-backing.ts +103 -0
- package/src/firestore/sandbox/reads.ts +18 -0
- package/src/firestore/sandbox/request-events.ts +119 -0
- package/src/firestore/sandbox/rules-evaluation.ts +95 -0
- package/src/firestore/sandbox/rules-read-engine.ts +499 -0
- package/src/firestore/sandbox/rules-state.ts +61 -0
- package/src/firestore/sandbox/sentinel-capture.ts +63 -0
- package/src/firestore/sandbox/snapshot-listeners.ts +482 -0
- package/src/firestore/sandbox/topk.ts +38 -0
- package/src/firestore/sandbox/transaction-merge.ts +157 -0
- package/src/firestore/sandbox/transaction-types.ts +160 -0
- package/src/firestore/sandbox/transaction.ts +143 -0
- package/src/firestore/sandbox/trigger-scope.ts +52 -0
- package/src/firestore/sandbox/value-equality.ts +34 -0
- package/src/firestore/sandbox/value-resolver.ts +289 -0
- package/src/firestore/sandbox/wire-encoder.ts +136 -0
- package/src/firestore/sandbox/writes.ts +91 -0
- package/src/firestore/sandbox-controls.ts +143 -0
- package/src/firestore/snapshots.ts +188 -0
- package/src/firestore/state.ts +341 -0
- package/src/firestore/tools.ts +386 -0
- package/src/firestore/transactions.ts +32 -0
- package/src/firestore/types.ts +119 -0
- package/src/firestore/writes.ts +106 -0
- package/src/messaging/broker/broker.test.ts +260 -0
- package/src/messaging/broker/broker.ts +482 -0
- package/src/messaging/broker/envelopes.ts +177 -0
- package/src/messaging/broker/index.ts +72 -0
- package/src/messaging/broker/tokens.ts +39 -0
- package/src/messaging/broker/types.ts +104 -0
- package/src/messaging/broker/validate.ts +206 -0
- package/src/messaging/client.ts +160 -0
- package/src/messaging/index.ts +2 -0
- package/src/messaging/instance.ts +199 -0
- package/src/messaging/internal.ts +20 -0
- package/src/messaging/sw.ts +110 -0
- package/src/project-scope.ts +4 -0
- package/src/rules/api/assert.ts +130 -0
- package/src/rules/api/case-types.ts +158 -0
- package/src/rules/api/errors.ts +51 -0
- package/src/rules/api/firestore.ts +173 -0
- package/src/rules/api/issue.ts +101 -0
- package/src/rules/api/lint.ts +69 -0
- package/src/rules/api/rtdb.ts +213 -0
- package/src/rules/api/values.ts +73 -0
- package/src/rules/generators/assembler.ts +325 -0
- package/src/rules/generators/expressions.ts +125 -0
- package/src/rules/generators/grid.ts +63 -0
- package/src/rules/grammar/FirestoreAST.ts +93 -0
- package/src/rules/grammar/FirestoreAssembler.ts +200 -0
- package/src/rules/grammar/FirestoreParser.ts +431 -0
- package/src/rules/grammar/FirestoreRules.ohm +236 -0
- package/src/rules/grammar/FirestoreRules.ohm.generated.ts +241 -0
- package/src/rules/grammar/FirestoreValidator.ts +502 -0
- package/src/rules/index.ts +151 -0
- package/src/rules/indexes/extract/annotation-collect.ts +90 -0
- package/src/rules/indexes/extract/annotations.ts +177 -0
- package/src/rules/indexes/extract/ast.ts +118 -0
- package/src/rules/indexes/extract/classify.ts +163 -0
- package/src/rules/indexes/extract/composite.ts +112 -0
- package/src/rules/indexes/extract/dataflow.ts +258 -0
- package/src/rules/indexes/extract/enumerate.ts +194 -0
- package/src/rules/indexes/extract/extractor.ts +311 -0
- package/src/rules/indexes/extract/types.ts +222 -0
- package/src/rules/indexes/extractHandler.ts +110 -0
- package/src/rules/indexes/extractTool.ts +73 -0
- package/src/rules/indexes/types.ts +83 -0
- package/src/rules/inspect/handler.ts +174 -0
- package/src/rules/inspect/spec.ts +35 -0
- package/src/rules/inspect/tools.ts +42 -0
- package/src/rules/internal/extract.ts +51 -0
- package/src/rules/internal/index.ts +239 -0
- package/src/rules/internal/node.ts +44 -0
- package/src/rules/internal/rtdb.ts +108 -0
- package/src/rules/linter/ast-utils.ts +401 -0
- package/src/rules/linter/hallucinations.ts +373 -0
- package/src/rules/linter/linter.ts +904 -0
- package/src/rules/modules/resolver-browser.ts +71 -0
- package/src/rules/modules/resolver-core.ts +382 -0
- package/src/rules/modules/resolver.ts +62 -0
- package/src/rules/modules/stdlib/STDLIB.md +271 -0
- package/src/rules/modules/stdlib/atomic.rules +52 -0
- package/src/rules/modules/stdlib/atomic.test.json +152 -0
- package/src/rules/modules/stdlib/auth.rules +7 -0
- package/src/rules/modules/stdlib/auth.test.json +53 -0
- package/src/rules/modules/stdlib/content.rules +50 -0
- package/src/rules/modules/stdlib/content.test.json +238 -0
- package/src/rules/modules/stdlib/counters.rules +41 -0
- package/src/rules/modules/stdlib/counters.test.json +254 -0
- package/src/rules/modules/stdlib/geometry.rules +43 -0
- package/src/rules/modules/stdlib/geometry.test.json +200 -0
- package/src/rules/modules/stdlib/joining.rules +49 -0
- package/src/rules/modules/stdlib/joining.test.json +287 -0
- package/src/rules/modules/stdlib/lifecycle.rules +51 -0
- package/src/rules/modules/stdlib/lifecycle.test.json +288 -0
- package/src/rules/modules/stdlib/lobby.rules +44 -0
- package/src/rules/modules/stdlib/lobby.test.json +32 -0
- package/src/rules/modules/stdlib/membership.rules +40 -0
- package/src/rules/modules/stdlib/membership.test.json +107 -0
- package/src/rules/modules/stdlib/spaces.rules +46 -0
- package/src/rules/modules/stdlib/spaces.test.json +392 -0
- package/src/rules/modules/stdlib/state.rules +28 -0
- package/src/rules/modules/stdlib/state.test.json +55 -0
- package/src/rules/modules/stdlib/timing.rules +20 -0
- package/src/rules/modules/stdlib/timing.test.json +106 -0
- package/src/rules/modules/stdlib/transitions.rules +34 -0
- package/src/rules/modules/stdlib/transitions.test.json +86 -0
- package/src/rules/modules/stdlib/turns.rules +27 -0
- package/src/rules/modules/stdlib/turns.test.json +64 -0
- package/src/rules/modules/stdlib/validation.rules +31 -0
- package/src/rules/modules/stdlib/validation.test.json +184 -0
- package/src/rules/modules/stdlib-content.ts +590 -0
- package/src/rules/rtdb/compiled-rules.ts +121 -0
- package/src/rules/rtdb/constraints/atoms.spec.ts +47 -0
- package/src/rules/rtdb/constraints/atoms.ts +69 -0
- package/src/rules/rtdb/constraints/compose.ts +24 -0
- package/src/rules/rtdb/constraints/data.ts +79 -0
- package/src/rules/rtdb/constraints/document.ts +162 -0
- package/src/rules/rtdb/constraints/game.spec.ts +32 -0
- package/src/rules/rtdb/constraints/game.ts +70 -0
- package/src/rules/rtdb/constraints/index.ts +32 -0
- package/src/rules/rtdb/constraints/policies.spec.ts +34 -0
- package/src/rules/rtdb/constraints/policies.ts +36 -0
- package/src/rules/rtdb/constraints/ruleset.ts +151 -0
- package/src/rules/rtdb/constraints/schema.ts +89 -0
- package/src/rules/rtdb/constraints/types.ts +23 -0
- package/src/rules/rtdb/constraints/write-rules-file.ts +35 -0
- package/src/rules/rtdb/expression-engine.ts +24 -0
- package/src/rules/rtdb/grammar/RtdbExpr.ohm +98 -0
- package/src/rules/rtdb/grammar/RtdbExpr.ohm.generated.ts +103 -0
- package/src/rules/rtdb/grammar/RtdbExprParser.ts +65 -0
- package/src/rules/rtdb/grammar/linter.ts +90 -0
- package/src/rules/rtdb/grammar/simulator.ts +340 -0
- package/src/rules/rtdb/grammar/validator.ts +113 -0
- package/src/rules/rtdb/simulation/handler.ts +430 -0
- package/src/rules/rtdb/simulation/spec.ts +48 -0
- package/src/rules/rtdb/types.ts +39 -0
- package/src/rules/simulator/evaluator.ts +1461 -0
- package/src/rules/simulator/expression/eval-errors.ts +136 -0
- package/src/rules/simulator/expression/evaluator.ts +425 -0
- package/src/rules/simulator/expression/lexer.ts +395 -0
- package/src/rules/simulator/expression/parser.ts +375 -0
- package/src/rules/simulator/expression/types.ts +252 -0
- package/src/rules/simulator/expression/walk-data.ts +216 -0
- package/src/rules/simulator/firestore-set.ts +90 -0
- package/src/rules/simulator/handler.ts +739 -0
- package/src/rules/simulator/mapdiff.ts +78 -0
- package/src/rules/simulator/project-after-state.ts +125 -0
- package/src/rules/simulator/query-proof.ts +513 -0
- package/src/rules/simulator/value-equality.ts +33 -0
- package/src/rules/simulator/wrappers/base.ts +143 -0
- package/src/rules/simulator/wrappers/bytes.ts +142 -0
- package/src/rules/simulator/wrappers/duration.ts +191 -0
- package/src/rules/simulator/wrappers/float.ts +92 -0
- package/src/rules/simulator/wrappers/latlng.ts +116 -0
- package/src/rules/simulator/wrappers/path.ts +166 -0
- package/src/rules/simulator/wrappers/reference.ts +161 -0
- package/src/rules/simulator/wrappers/timestamp.ts +228 -0
- package/src/rules/simulator/wrappers/vector.ts +88 -0
- package/src/rules/simulator-tools-impl.ts +544 -0
- package/src/rules/simulator.ts +36 -0
- package/src/rules/stdlib-modules.ts +1023 -0
- package/src/rules/stdlib-tools.ts +206 -0
- package/src/rules/test/handler.ts +272 -0
- package/src/rules/test/spec.ts +681 -0
- package/src/rules/tools.ts +159 -0
- package/src/rules/write/handler.ts +140 -0
- package/src/rules/write/spec.ts +7 -0
- package/src/sandbox/admin-compat.ts +39 -0
- package/src/sandbox/admin-firestore/error-translation.ts +254 -0
- package/src/sandbox/admin-firestore/get-admin-firestore.ts +104 -0
- package/src/sandbox/admin-firestore/get-firestore.ts +73 -0
- package/src/sandbox/admin-firestore/index.ts +96 -0
- package/src/sandbox/admin-firestore/listeners.ts +400 -0
- package/src/sandbox/admin-firestore/local-handle.ts +98 -0
- package/src/sandbox/admin-firestore/remote/batch.ts +41 -0
- package/src/sandbox/admin-firestore/remote/channel.ts +34 -0
- package/src/sandbox/admin-firestore/remote/doc-ref.ts +112 -0
- package/src/sandbox/admin-firestore/remote/errors.ts +38 -0
- package/src/sandbox/admin-firestore/remote/listeners.ts +130 -0
- package/src/sandbox/admin-firestore/remote/query.ts +245 -0
- package/src/sandbox/admin-firestore/remote/remote-firestore.ts +148 -0
- package/src/sandbox/admin-firestore/remote/snapshots.ts +50 -0
- package/src/sandbox/admin-firestore/remote/transaction.ts +128 -0
- package/src/sandbox/admin-firestore/remote/value-codec.ts +134 -0
- package/src/sandbox/admin-firestore/remote/wire-types.ts +65 -0
- package/src/sandbox/admin-firestore/types.ts +62 -0
- package/src/sandbox/branches/index.ts +378 -0
- package/src/sandbox/index.ts +141 -0
- package/src/sandbox/internal/client-app.ts +49 -0
- package/src/sandbox/internal/firebase-error.ts +23 -0
- package/src/sandbox/internal/index.ts +96 -0
- package/src/sandbox/internal/local-brand.ts +2 -0
- package/src/sandbox/internal/provenance.ts +176 -0
- package/src/sandbox/internal/sandbox-impl.ts +837 -0
- package/src/sandbox/operation-record.ts +194 -0
- package/src/sandbox/persistence/backends.ts +229 -0
- package/src/sandbox/persistence/chunk-format.ts +195 -0
- package/src/sandbox/persistence/controller.ts +537 -0
- package/src/sandbox/persistence/index.ts +12 -0
- package/src/sandbox/persistence/serialize.ts +131 -0
- package/src/sandbox/persistence/types.ts +118 -0
- package/src/sandbox/remote.ts +137 -0
- package/src/sandbox/replay/index.ts +326 -0
- package/src/sandbox/sandbox-context.ts +117 -0
- package/src/sandbox/tab-sync/index.ts +371 -0
- package/src/sandbox/types/auth-state.ts +22 -0
- package/src/sandbox/types/context.ts +34 -0
- package/src/sandbox/types/errors.ts +160 -0
- package/src/sandbox/types/events.ts +570 -0
- package/src/sandbox/types/index.ts +61 -0
- package/src/sandbox/types/operation.ts +64 -0
- package/src/sandbox/types/persistence.ts +150 -0
- package/src/sandbox/types/service.ts +392 -0
- package/src/storage/admin/api.ts +601 -0
- package/src/storage/admin/handler.ts +125 -0
- package/src/storage/admin/spec.ts +73 -0
- package/src/storage/admin/tools.ts +115 -0
- package/src/storage/download.ts +162 -0
- package/src/storage/enforce.ts +174 -0
- package/src/storage/errors.ts +95 -0
- package/src/storage/index.ts +95 -0
- package/src/storage/instances.ts +42 -0
- package/src/storage/internal.ts +21 -0
- package/src/storage/list.ts +96 -0
- package/src/storage/metadata.ts +224 -0
- package/src/storage/persistence.ts +282 -0
- package/src/storage/reference.ts +134 -0
- package/src/storage/rules.ts +1544 -0
- package/src/storage/service.ts +389 -0
- package/src/storage/upload.ts +291 -0
- package/dist/firestore-values/index.d.ts.map +0 -1
- package/dist/firestore-values/index.js.map +0 -1
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The OpenAI AnswerEngine: Gemini wire in, OpenAI-compatible upstream
|
|
3
|
+
* (Ollama, llama.cpp, any /v1/chat/completions) out, Gemini wire back.
|
|
4
|
+
*
|
|
5
|
+
* The translation core was validated end-to-end against the real firebase/ai
|
|
6
|
+
* 2.12.0 SDK before landing here as pure, unit-testable functions; only
|
|
7
|
+
* `OpenAiEngine` does I/O.
|
|
8
|
+
*
|
|
9
|
+
* The five lossy edges that validation pinned, all handled here:
|
|
10
|
+
* 1. Gemini functionResponse has no tool_call_id → synthesized FIFO id
|
|
11
|
+
* matching per function name ({@link geminiToOpenAIRequest}).
|
|
12
|
+
* 2. OpenAI streams tool-call ARGUMENT FRAGMENTS; Gemini streams WHOLE
|
|
13
|
+
* functionCall parts → {@link ToolCallBuffer} accumulates fragments and
|
|
14
|
+
* the engine emits whole parts on the finish chunk.
|
|
15
|
+
* 3. Never forward OpenAI's `data: [DONE]` sentinel — the SDK JSON.parses
|
|
16
|
+
* every event and dies with PARSE_FAILED (engine skips it).
|
|
17
|
+
* 4. Thought parts are SKIPPED when replaying history to OpenAI
|
|
18
|
+
* (`part.thought` filter in the request walk).
|
|
19
|
+
* 5. thoughtSignature has no OpenAI equivalent → minted on the Gemini
|
|
20
|
+
* side by the synthesizer's decoration pass (production signs even
|
|
21
|
+
* trivial text parts, and rejects unsigned functionCall turns).
|
|
22
|
+
*
|
|
23
|
+
* Plus: `content_filter` maps to SAFETY (faithful — makes response.text()
|
|
24
|
+
* throw exactly like production).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
AiBrokerError,
|
|
29
|
+
Synthesizer,
|
|
30
|
+
errorEnvelope,
|
|
31
|
+
estimateTokens,
|
|
32
|
+
resolveModelVersion,
|
|
33
|
+
} from './synthesizer.js';
|
|
34
|
+
import { promptTextOf } from './scripted-engine.js';
|
|
35
|
+
import type {
|
|
36
|
+
AnswerEngine,
|
|
37
|
+
CountTokensRequest,
|
|
38
|
+
CountTokensResponse,
|
|
39
|
+
GenerateContentRequest,
|
|
40
|
+
WireChunk,
|
|
41
|
+
WirePart,
|
|
42
|
+
WireResponse,
|
|
43
|
+
} from './types.js';
|
|
44
|
+
|
|
45
|
+
// ── OpenAI-side wire types (minimal) ────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
export interface OpenAIToolCall {
|
|
48
|
+
id: string;
|
|
49
|
+
type: 'function';
|
|
50
|
+
function: { name: string; arguments: string };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface OpenAIMessage {
|
|
54
|
+
role: 'system' | 'user' | 'assistant' | 'tool';
|
|
55
|
+
content: string | null;
|
|
56
|
+
tool_calls?: OpenAIToolCall[];
|
|
57
|
+
tool_call_id?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface OpenAIRequest {
|
|
61
|
+
model: string;
|
|
62
|
+
messages: OpenAIMessage[];
|
|
63
|
+
stream?: boolean;
|
|
64
|
+
stream_options?: { include_usage: boolean };
|
|
65
|
+
tools?: Array<{ type: 'function'; function: Record<string, unknown> }>;
|
|
66
|
+
tool_choice?: string;
|
|
67
|
+
temperature?: number;
|
|
68
|
+
top_p?: number;
|
|
69
|
+
max_tokens?: number;
|
|
70
|
+
stop?: string[];
|
|
71
|
+
response_format?:
|
|
72
|
+
| { type: 'json_object' }
|
|
73
|
+
| {
|
|
74
|
+
type: 'json_schema';
|
|
75
|
+
json_schema: { name: string; schema: Record<string, unknown>; strict?: boolean };
|
|
76
|
+
};
|
|
77
|
+
frequency_penalty?: number;
|
|
78
|
+
presence_penalty?: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface OpenAIResponse {
|
|
82
|
+
id: string;
|
|
83
|
+
model: string;
|
|
84
|
+
choices: Array<{
|
|
85
|
+
index: number;
|
|
86
|
+
message: {
|
|
87
|
+
role: string;
|
|
88
|
+
content: string | null;
|
|
89
|
+
tool_calls?: OpenAIToolCall[];
|
|
90
|
+
reasoning?: string;
|
|
91
|
+
reasoning_content?: string;
|
|
92
|
+
};
|
|
93
|
+
finish_reason: string | null;
|
|
94
|
+
}>;
|
|
95
|
+
usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface OpenAIStreamChunk {
|
|
99
|
+
id: string;
|
|
100
|
+
model: string;
|
|
101
|
+
choices: Array<{
|
|
102
|
+
index: number;
|
|
103
|
+
delta: {
|
|
104
|
+
role?: string;
|
|
105
|
+
content?: string;
|
|
106
|
+
reasoning?: string;
|
|
107
|
+
reasoning_content?: string;
|
|
108
|
+
tool_calls?: Array<Partial<OpenAIToolCall> & { index: number }>;
|
|
109
|
+
};
|
|
110
|
+
finish_reason: string | null;
|
|
111
|
+
}>;
|
|
112
|
+
usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── Request translation: Gemini → OpenAI (pure) ─────────────────────────────
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Gemini functionResponse parts carry only a function *name*; OpenAI tool
|
|
119
|
+
* messages require the tool_call_id of the originating call. We synthesize
|
|
120
|
+
* deterministic ids when translating the model's functionCall parts and
|
|
121
|
+
* look them up by name (FIFO per name) for the responses.
|
|
122
|
+
*/
|
|
123
|
+
function synthToolCallId(name: string, ordinal: number): string {
|
|
124
|
+
return `call_${name}_${ordinal}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function geminiToOpenAIRequest(
|
|
128
|
+
model: string,
|
|
129
|
+
req: GenerateContentRequest,
|
|
130
|
+
stream: boolean,
|
|
131
|
+
): OpenAIRequest {
|
|
132
|
+
const messages: OpenAIMessage[] = [];
|
|
133
|
+
|
|
134
|
+
// systemInstruction -> leading system message
|
|
135
|
+
if (req.systemInstruction) {
|
|
136
|
+
const text = (req.systemInstruction.parts ?? []).map((p) => p.text ?? '').join('');
|
|
137
|
+
if (text) messages.push({ role: 'system', content: text });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Walk contents. Track pending tool-call ids per function name so a later
|
|
141
|
+
// functionResponse turn can reference the right id.
|
|
142
|
+
const pendingIdsByName = new Map<string, string[]>();
|
|
143
|
+
let callOrdinal = 0;
|
|
144
|
+
|
|
145
|
+
for (const content of req.contents ?? []) {
|
|
146
|
+
if (content.role === 'function') {
|
|
147
|
+
// functionResponse turn -> one OpenAI `tool` message per part
|
|
148
|
+
for (const part of content.parts) {
|
|
149
|
+
if (!part.functionResponse) continue;
|
|
150
|
+
const queue = pendingIdsByName.get(part.functionResponse.name) ?? [];
|
|
151
|
+
const id = queue.shift() ?? synthToolCallId(part.functionResponse.name, callOrdinal++);
|
|
152
|
+
messages.push({
|
|
153
|
+
role: 'tool',
|
|
154
|
+
tool_call_id: id,
|
|
155
|
+
content: JSON.stringify(part.functionResponse.response),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const role = content.role === 'model' ? 'assistant' : 'user';
|
|
162
|
+
const textParts: string[] = [];
|
|
163
|
+
const toolCalls: OpenAIToolCall[] = [];
|
|
164
|
+
|
|
165
|
+
for (const part of content.parts) {
|
|
166
|
+
// LOSSY EDGE: thought parts must not be replayed to OpenAI as
|
|
167
|
+
// assistant text; real Gemini also does not accept thought text back
|
|
168
|
+
// (only thoughtSignatures — which have no OpenAI equivalent and drop).
|
|
169
|
+
if (part.thought) continue;
|
|
170
|
+
if (typeof part.text === 'string') {
|
|
171
|
+
textParts.push(part.text);
|
|
172
|
+
} else if (part.functionCall) {
|
|
173
|
+
const id = synthToolCallId(part.functionCall.name, callOrdinal++);
|
|
174
|
+
const queue = pendingIdsByName.get(part.functionCall.name) ?? [];
|
|
175
|
+
queue.push(id);
|
|
176
|
+
pendingIdsByName.set(part.functionCall.name, queue);
|
|
177
|
+
toolCalls.push({
|
|
178
|
+
id,
|
|
179
|
+
type: 'function',
|
|
180
|
+
// LOSSY EDGE: Gemini args is an object; OpenAI wants a JSON string.
|
|
181
|
+
function: {
|
|
182
|
+
name: part.functionCall.name,
|
|
183
|
+
arguments: JSON.stringify(part.functionCall.args ?? {}),
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
} else if (part.inlineData) {
|
|
187
|
+
// Not exercised by the V1 broker; marked gap carried from the prototype.
|
|
188
|
+
textParts.push(`[unsupported inlineData part: ${part.inlineData.mimeType}]`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const msg: OpenAIMessage = {
|
|
193
|
+
role,
|
|
194
|
+
content: textParts.length ? textParts.join('') : null,
|
|
195
|
+
};
|
|
196
|
+
if (toolCalls.length) msg.tool_calls = toolCalls;
|
|
197
|
+
messages.push(msg);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const out: OpenAIRequest = { model, messages, stream };
|
|
201
|
+
if (stream) out.stream_options = { include_usage: true };
|
|
202
|
+
|
|
203
|
+
// tools
|
|
204
|
+
const decls = (req.tools ?? []).flatMap((t) => t.functionDeclarations ?? []);
|
|
205
|
+
if (decls.length) {
|
|
206
|
+
out.tools = decls.map((d) => ({
|
|
207
|
+
type: 'function',
|
|
208
|
+
function: {
|
|
209
|
+
name: d.name,
|
|
210
|
+
description: d.description,
|
|
211
|
+
parameters: normalizeSchema(d.parameters) ?? { type: 'object', properties: {} },
|
|
212
|
+
},
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// toolConfig.mode -> tool_choice
|
|
217
|
+
const mode = req.toolConfig?.functionCallingConfig?.mode;
|
|
218
|
+
if (mode === 'ANY') out.tool_choice = 'required';
|
|
219
|
+
else if (mode === 'NONE') out.tool_choice = 'none';
|
|
220
|
+
else if (mode === 'AUTO') out.tool_choice = 'auto';
|
|
221
|
+
|
|
222
|
+
// generationConfig
|
|
223
|
+
const g = req.generationConfig;
|
|
224
|
+
if (g) {
|
|
225
|
+
if (g.temperature !== undefined) out.temperature = g.temperature;
|
|
226
|
+
if (g.topP !== undefined) out.top_p = g.topP;
|
|
227
|
+
if (g.maxOutputTokens !== undefined) out.max_tokens = g.maxOutputTokens;
|
|
228
|
+
if (g.stopSequences?.length) out.stop = g.stopSequences;
|
|
229
|
+
if (g.frequencyPenalty !== undefined) out.frequency_penalty = g.frequencyPenalty;
|
|
230
|
+
if (g.presencePenalty !== undefined) out.presence_penalty = g.presencePenalty;
|
|
231
|
+
// LOSSY EDGE: topK has no OpenAI equivalent; dropped.
|
|
232
|
+
if (g.responseMimeType === 'application/json') {
|
|
233
|
+
out.response_format = g.responseSchema
|
|
234
|
+
? {
|
|
235
|
+
type: 'json_schema',
|
|
236
|
+
json_schema: {
|
|
237
|
+
name: 'response',
|
|
238
|
+
schema: normalizeSchema(g.responseSchema)!,
|
|
239
|
+
strict: true,
|
|
240
|
+
},
|
|
241
|
+
}
|
|
242
|
+
: { type: 'json_object' };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Gemini schemas allow uppercase type names ("STRING") and a `nullable`
|
|
251
|
+
* flag; OpenAI/JSON-Schema wants lowercase types. Normalize defensively and
|
|
252
|
+
* strip fields JSON Schema validators reject.
|
|
253
|
+
*/
|
|
254
|
+
export function normalizeSchema(schema: unknown): Record<string, unknown> | undefined {
|
|
255
|
+
if (!schema || typeof schema !== 'object') return undefined;
|
|
256
|
+
const s = { ...(schema as Record<string, unknown>) };
|
|
257
|
+
if (typeof s.type === 'string') s.type = (s.type as string).toLowerCase();
|
|
258
|
+
delete s.nullable;
|
|
259
|
+
if (s.properties && typeof s.properties === 'object') {
|
|
260
|
+
s.properties = Object.fromEntries(
|
|
261
|
+
Object.entries(s.properties as Record<string, unknown>).map(([k, v]) => [
|
|
262
|
+
k,
|
|
263
|
+
normalizeSchema(v),
|
|
264
|
+
]),
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
if (s.items) s.items = normalizeSchema(s.items);
|
|
268
|
+
return s;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ── Response translation: OpenAI → Gemini (pure) ────────────────────────────
|
|
272
|
+
|
|
273
|
+
const FINISH_REASON_MAP: Record<string, string> = {
|
|
274
|
+
stop: 'STOP',
|
|
275
|
+
length: 'MAX_TOKENS',
|
|
276
|
+
// A tool call is a normal, expected stop in Gemini's model; mapping to
|
|
277
|
+
// anything in badFinishReasons would make the SDK throw on response.text().
|
|
278
|
+
tool_calls: 'STOP',
|
|
279
|
+
// Faithful mapping: SAFETY makes the SDK's response.text() throw — exactly
|
|
280
|
+
// the real backend's behavior for filtered content.
|
|
281
|
+
content_filter: 'SAFETY',
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
export function mapFinishReason(reason: string | null | undefined): string {
|
|
285
|
+
if (!reason) return 'STOP';
|
|
286
|
+
return FINISH_REASON_MAP[reason] ?? 'OTHER';
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function messagePartsFromOpenAI(msg: {
|
|
290
|
+
content: string | null;
|
|
291
|
+
tool_calls?: OpenAIToolCall[];
|
|
292
|
+
reasoning?: string;
|
|
293
|
+
reasoning_content?: string;
|
|
294
|
+
}): WirePart[] {
|
|
295
|
+
const parts: WirePart[] = [];
|
|
296
|
+
// Ollama's OpenAI compat surfaces thinking-model output in a nonstandard
|
|
297
|
+
// `reasoning` field; llama-server / deepseek-format servers use
|
|
298
|
+
// `reasoning_content` instead. Gemini represents either as a thought part.
|
|
299
|
+
// `reasoning` wins if both are somehow present (shouldn't happen in practice).
|
|
300
|
+
const reasoning = msg.reasoning ?? msg.reasoning_content;
|
|
301
|
+
if (reasoning) parts.push({ text: reasoning, thought: true });
|
|
302
|
+
if (msg.content) parts.push({ text: msg.content });
|
|
303
|
+
for (const tc of msg.tool_calls ?? []) {
|
|
304
|
+
let args: Record<string, unknown> = {};
|
|
305
|
+
try {
|
|
306
|
+
// LOSSY EDGE in reverse: OpenAI arguments is a JSON *string*; Gemini
|
|
307
|
+
// wants an object.
|
|
308
|
+
args = JSON.parse(tc.function.arguments || '{}');
|
|
309
|
+
} catch {
|
|
310
|
+
args = { __malformed: tc.function.arguments };
|
|
311
|
+
}
|
|
312
|
+
parts.push({ functionCall: { name: tc.function.name, args } });
|
|
313
|
+
}
|
|
314
|
+
if (!parts.length) parts.push({ text: '' });
|
|
315
|
+
return parts;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function openAIToGeminiResponse(resp: OpenAIResponse): WireResponse {
|
|
319
|
+
const choice = resp.choices[0]!;
|
|
320
|
+
const malformedToolArgs = (choice.message.tool_calls ?? []).some((tc) => {
|
|
321
|
+
try {
|
|
322
|
+
JSON.parse(tc.function.arguments || '{}');
|
|
323
|
+
return false;
|
|
324
|
+
} catch {
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
const out: WireResponse = {
|
|
329
|
+
candidates: [
|
|
330
|
+
{
|
|
331
|
+
index: 0,
|
|
332
|
+
content: { role: 'model', parts: messagePartsFromOpenAI(choice.message) },
|
|
333
|
+
finishReason: malformedToolArgs
|
|
334
|
+
? 'MALFORMED_FUNCTION_CALL'
|
|
335
|
+
: mapFinishReason(choice.finish_reason),
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
};
|
|
339
|
+
if (resp.usage) {
|
|
340
|
+
// Upstream-faithful counts only; the synthesizer's decorate pass adds the
|
|
341
|
+
// synthesized promptTokensDetails/serviceTier decoration.
|
|
342
|
+
out.usageMetadata = {
|
|
343
|
+
promptTokenCount: resp.usage.prompt_tokens,
|
|
344
|
+
candidatesTokenCount: resp.usage.completion_tokens,
|
|
345
|
+
totalTokenCount: resp.usage.total_tokens,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
return out;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* One OpenAI stream chunk → the TEXT parts of one Gemini chunk (or null for
|
|
353
|
+
* frames carrying nothing representable). Tool-call fragments are NOT
|
|
354
|
+
* handled here — they buffer in {@link ToolCallBuffer} and emit whole on the
|
|
355
|
+
* finish chunk (lossy edge #2).
|
|
356
|
+
*/
|
|
357
|
+
export function openAIChunkToParts(chunk: OpenAIStreamChunk): WirePart[] {
|
|
358
|
+
const choice = chunk.choices?.[0];
|
|
359
|
+
const parts: WirePart[] = [];
|
|
360
|
+
const reasoning = choice?.delta?.reasoning ?? choice?.delta?.reasoning_content;
|
|
361
|
+
if (reasoning) parts.push({ text: reasoning, thought: true });
|
|
362
|
+
if (choice?.delta?.content) parts.push({ text: choice.delta.content });
|
|
363
|
+
return parts;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Accumulates OpenAI streamed tool-call fragments (indexed deltas whose
|
|
368
|
+
* `arguments` arrive as string pieces) into whole Gemini functionCall parts.
|
|
369
|
+
*/
|
|
370
|
+
export class ToolCallBuffer {
|
|
371
|
+
private readonly byIndex = new Map<number, { name: string; args: string }>();
|
|
372
|
+
|
|
373
|
+
add(deltas: Array<Partial<OpenAIToolCall> & { index: number }> | undefined): void {
|
|
374
|
+
for (const d of deltas ?? []) {
|
|
375
|
+
const slot = this.byIndex.get(d.index) ?? { name: '', args: '' };
|
|
376
|
+
if (d.function?.name) slot.name += d.function.name;
|
|
377
|
+
if (d.function?.arguments) slot.args += d.function.arguments;
|
|
378
|
+
this.byIndex.set(d.index, slot);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
get size(): number {
|
|
383
|
+
return this.byIndex.size;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Emit whole functionCall parts (args parsed to an object) and reset. */
|
|
387
|
+
flush(): WirePart[] {
|
|
388
|
+
const parts: WirePart[] = [];
|
|
389
|
+
const indexes = [...this.byIndex.keys()].sort((a, b) => a - b);
|
|
390
|
+
for (const i of indexes) {
|
|
391
|
+
const { name, args } = this.byIndex.get(i)!;
|
|
392
|
+
let parsed: Record<string, unknown> = {};
|
|
393
|
+
try {
|
|
394
|
+
parsed = JSON.parse(args || '{}');
|
|
395
|
+
} catch {
|
|
396
|
+
parsed = { __malformed: args };
|
|
397
|
+
}
|
|
398
|
+
parts.push({ functionCall: { name, args: parsed } });
|
|
399
|
+
}
|
|
400
|
+
this.byIndex.clear();
|
|
401
|
+
return parts;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ── SSE parsing (pure, incremental) ─────────────────────────────────────────
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Incremental SSE `data:` payload extractor. Push decoded text as it
|
|
409
|
+
* arrives; get back complete event payload strings. Handles both LF LF and
|
|
410
|
+
* CRLF CRLF separators. Emits the `[DONE]` sentinel verbatim — CALLERS must
|
|
411
|
+
* skip it (lossy edge #3); the engine never forwards it downstream.
|
|
412
|
+
*/
|
|
413
|
+
export class SseParser {
|
|
414
|
+
private buffer = '';
|
|
415
|
+
|
|
416
|
+
push(text: string): string[] {
|
|
417
|
+
this.buffer += text;
|
|
418
|
+
const payloads: string[] = [];
|
|
419
|
+
for (;;) {
|
|
420
|
+
const lf = this.buffer.indexOf('\n\n');
|
|
421
|
+
const crlf = this.buffer.indexOf('\r\n\r\n');
|
|
422
|
+
const at = lf === -1 ? crlf : crlf === -1 ? lf : Math.min(lf, crlf);
|
|
423
|
+
if (at === -1) break;
|
|
424
|
+
const sepLen = at === crlf && crlf !== -1 && (lf === -1 || crlf < lf) ? 4 : 2;
|
|
425
|
+
const rawEvent = this.buffer.slice(0, at);
|
|
426
|
+
this.buffer = this.buffer.slice(at + sepLen);
|
|
427
|
+
for (const line of rawEvent.split(/\r?\n/)) {
|
|
428
|
+
if (line.startsWith('data: ')) payloads.push(line.slice('data: '.length));
|
|
429
|
+
else if (line.startsWith('data:')) payloads.push(line.slice('data:'.length));
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return payloads;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export const DONE_SENTINEL = '[DONE]';
|
|
437
|
+
|
|
438
|
+
// ── The engine ──────────────────────────────────────────────────────────────
|
|
439
|
+
|
|
440
|
+
export interface OpenAiEngineOptions {
|
|
441
|
+
baseUrl: string;
|
|
442
|
+
model?: string;
|
|
443
|
+
modelMap?: Record<string, string>;
|
|
444
|
+
fetch?: typeof fetch;
|
|
445
|
+
synthesizer?: Synthesizer;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export class OpenAiEngine implements AnswerEngine {
|
|
449
|
+
private readonly baseUrl: string;
|
|
450
|
+
private readonly defaultModel: string | undefined;
|
|
451
|
+
private readonly modelMap: Record<string, string>;
|
|
452
|
+
/** Call-signature only (not `typeof fetch`): the wrapper below carries none
|
|
453
|
+
* of the runtime's static props (e.g. Bun's `fetch.preconnect`). */
|
|
454
|
+
private readonly fetchImpl: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
455
|
+
private readonly synth: Synthesizer;
|
|
456
|
+
|
|
457
|
+
private warnedDroppedFields = false;
|
|
458
|
+
|
|
459
|
+
constructor(options: OpenAiEngineOptions) {
|
|
460
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, '');
|
|
461
|
+
this.defaultModel = options.model;
|
|
462
|
+
this.modelMap = options.modelMap ?? {};
|
|
463
|
+
// Wrapped, not stored bare: `this.fetchImpl(...)` would otherwise invoke
|
|
464
|
+
// the global fetch with the engine as its receiver, an Illegal invocation
|
|
465
|
+
// in browser/worker contexts (Node tolerates it, browsers do not).
|
|
466
|
+
this.fetchImpl = options.fetch ?? ((input, init) => fetch(input, init));
|
|
467
|
+
this.synth = options.synthesizer ?? new Synthesizer();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* `thinkingConfig` and `topK` are visible on the request HERE, before
|
|
472
|
+
* {@link geminiToOpenAIRequest} silently drops them (no OpenAI-compat
|
|
473
|
+
* equivalent for either). Fires once per engine instance; production still
|
|
474
|
+
* honors both fields — only the local openai engine does not.
|
|
475
|
+
*/
|
|
476
|
+
private warnDroppedFieldsOnce(req: GenerateContentRequest): void {
|
|
477
|
+
if (this.warnedDroppedFields) return;
|
|
478
|
+
const g = req.generationConfig;
|
|
479
|
+
const hasThinking = g?.thinkingConfig !== undefined;
|
|
480
|
+
const hasTopK = g?.topK !== undefined;
|
|
481
|
+
if (!hasThinking && !hasTopK) return;
|
|
482
|
+
this.warnedDroppedFields = true;
|
|
483
|
+
const fields = [hasThinking && 'thinkingConfig', hasTopK && 'topK'].filter(Boolean).join(' and ');
|
|
484
|
+
console.warn(
|
|
485
|
+
`[pyric/ai] the openai engine drops ${fields} — no OpenAI-compatible equivalent on the upstream request.\n` +
|
|
486
|
+
`Production still honors ${fields}; switch to a real Gemini-backed engine if this must take effect in development.`,
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** Gemini model id → upstream model: modelMap[model] ?? config.model ?? passthrough. */
|
|
491
|
+
resolveUpstreamModel(model: string): string {
|
|
492
|
+
const bare = model.startsWith('models/') ? model.slice('models/'.length) : model;
|
|
493
|
+
return this.modelMap[bare] ?? this.modelMap[model] ?? this.defaultModel ?? bare;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async generateContent(req: GenerateContentRequest, model: string): Promise<WireResponse> {
|
|
497
|
+
this.warnDroppedFieldsOnce(req);
|
|
498
|
+
const body = geminiToOpenAIRequest(this.resolveUpstreamModel(model), req, false);
|
|
499
|
+
const res = await this.post(body);
|
|
500
|
+
const upstream = (await res.json()) as OpenAIResponse;
|
|
501
|
+
const translated = openAIToGeminiResponse(upstream);
|
|
502
|
+
return this.synth.decorate(translated, { model, promptText: promptTextOf(req) });
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
streamGenerateContent(req: GenerateContentRequest, model: string): AsyncIterable<WireChunk> {
|
|
506
|
+
this.warnDroppedFieldsOnce(req);
|
|
507
|
+
const body = geminiToOpenAIRequest(this.resolveUpstreamModel(model), req, true);
|
|
508
|
+
const opts = { model, promptText: promptTextOf(req) };
|
|
509
|
+
const synth = this.synth;
|
|
510
|
+
const post = () => this.post(body);
|
|
511
|
+
|
|
512
|
+
return (async function* stream(): AsyncGenerator<WireChunk> {
|
|
513
|
+
const res = await post();
|
|
514
|
+
if (!res.body) throw new AiBrokerError(errorEnvelope(502, 'upstream returned no body', 'UNAVAILABLE'));
|
|
515
|
+
|
|
516
|
+
const modelVersion = resolveModelVersion(model);
|
|
517
|
+
const responseId = synth.nextResponseId();
|
|
518
|
+
const promptTokenCount = estimateTokens(opts.promptText);
|
|
519
|
+
const toolBuffer = new ToolCallBuffer();
|
|
520
|
+
const parser = new SseParser();
|
|
521
|
+
const decoder = new TextDecoder();
|
|
522
|
+
let emittedText = '';
|
|
523
|
+
let upstreamUsage: OpenAIStreamChunk['usage'];
|
|
524
|
+
let pendingFinish: string | null = null;
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Frame one complete Gemini chunk. Wire-true semantics require
|
|
528
|
+
* usageMetadata on EVERY chunk, but OpenAI upstreams only send usage on
|
|
529
|
+
* the final frame (stream_options.include_usage) — so interim chunks
|
|
530
|
+
* carry synthesized estimates (by-design synthesized decoration,
|
|
531
|
+
* cdd-deltas #99.2) and the finish chunk uses real upstream numbers
|
|
532
|
+
* when they arrived.
|
|
533
|
+
*/
|
|
534
|
+
function frame(parts: WirePart[], finishReason?: string): WireChunk {
|
|
535
|
+
const candidatesTokenCount = estimateTokens(emittedText);
|
|
536
|
+
const usage = upstreamUsage
|
|
537
|
+
? {
|
|
538
|
+
promptTokenCount: upstreamUsage.prompt_tokens,
|
|
539
|
+
candidatesTokenCount: upstreamUsage.completion_tokens,
|
|
540
|
+
totalTokenCount: upstreamUsage.total_tokens,
|
|
541
|
+
}
|
|
542
|
+
: {
|
|
543
|
+
promptTokenCount,
|
|
544
|
+
candidatesTokenCount,
|
|
545
|
+
totalTokenCount: promptTokenCount + candidatesTokenCount,
|
|
546
|
+
};
|
|
547
|
+
return {
|
|
548
|
+
candidates: [
|
|
549
|
+
{
|
|
550
|
+
content: { parts: parts.length ? parts : [{ text: '' }], role: 'model' },
|
|
551
|
+
...(finishReason ? { finishReason } : {}),
|
|
552
|
+
index: 0,
|
|
553
|
+
},
|
|
554
|
+
],
|
|
555
|
+
usageMetadata: {
|
|
556
|
+
...usage,
|
|
557
|
+
promptTokensDetails: [{ modality: 'TEXT', tokenCount: usage.promptTokenCount }],
|
|
558
|
+
serviceTier: 'standard',
|
|
559
|
+
},
|
|
560
|
+
modelVersion,
|
|
561
|
+
responseId,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const reader = res.body.getReader();
|
|
566
|
+
try {
|
|
567
|
+
for (;;) {
|
|
568
|
+
const { done, value } = await reader.read();
|
|
569
|
+
const payloads = done
|
|
570
|
+
? parser.push('\n\n') // flush any trailing buffered event
|
|
571
|
+
: parser.push(decoder.decode(value, { stream: true }));
|
|
572
|
+
|
|
573
|
+
for (const payload of payloads) {
|
|
574
|
+
// LOSSY EDGE: never forward the [DONE] sentinel.
|
|
575
|
+
if (payload.trim() === DONE_SENTINEL) continue;
|
|
576
|
+
let chunk: OpenAIStreamChunk;
|
|
577
|
+
try {
|
|
578
|
+
chunk = JSON.parse(payload) as OpenAIStreamChunk;
|
|
579
|
+
} catch {
|
|
580
|
+
continue; // non-JSON keepalive/comment frames
|
|
581
|
+
}
|
|
582
|
+
if (chunk.usage) upstreamUsage = chunk.usage;
|
|
583
|
+
const choice = chunk.choices?.[0];
|
|
584
|
+
if (!choice) continue; // usage-only final frame folds into the finish chunk
|
|
585
|
+
|
|
586
|
+
toolBuffer.add(choice.delta?.tool_calls);
|
|
587
|
+
const parts = openAIChunkToParts(chunk);
|
|
588
|
+
emittedText += parts.map((p) => (p.thought ? '' : (p.text ?? ''))).join('');
|
|
589
|
+
|
|
590
|
+
if (choice.finish_reason != null) {
|
|
591
|
+
pendingFinish = choice.finish_reason;
|
|
592
|
+
// Hold the finish frame until the reader drains, so a trailing
|
|
593
|
+
// usage-only frame (stream_options.include_usage) lands in it.
|
|
594
|
+
if (parts.length) yield frame(parts);
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
if (parts.length) yield frame(parts);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (done) break;
|
|
601
|
+
}
|
|
602
|
+
} finally {
|
|
603
|
+
reader.releaseLock();
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// The stream always ends with ONE finishing chunk (mirrors the captured
|
|
607
|
+
// final frame: empty text part, signed, finishReason present).
|
|
608
|
+
// LOSSY EDGE: buffered tool-call fragments emit here as WHOLE
|
|
609
|
+
// functionCall parts, exactly once.
|
|
610
|
+
const flushed = toolBuffer.flush();
|
|
611
|
+
const finalParts = synth.signParts(flushed.length ? flushed : [{ text: '' }]);
|
|
612
|
+
yield frame(finalParts, mapFinishReason(pendingFinish));
|
|
613
|
+
})();
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/** No OpenAI equivalent — deterministic synthesized estimate, wire-true envelope. */
|
|
617
|
+
async countTokens(req: CountTokensRequest, _model: string): Promise<CountTokensResponse> {
|
|
618
|
+
return this.synth.countTokens(promptTextOf(req));
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
private async post(body: OpenAIRequest): Promise<Response> {
|
|
622
|
+
let res: Response;
|
|
623
|
+
try {
|
|
624
|
+
res = await this.fetchImpl(`${this.baseUrl}/chat/completions`, {
|
|
625
|
+
method: 'POST',
|
|
626
|
+
headers: { 'content-type': 'application/json' },
|
|
627
|
+
body: JSON.stringify(body),
|
|
628
|
+
});
|
|
629
|
+
} catch (err) {
|
|
630
|
+
throw new AiBrokerError(
|
|
631
|
+
errorEnvelope(502, `openai engine: upstream fetch failed: ${String(err)}`, 'UNAVAILABLE'),
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
if (!res.ok) {
|
|
635
|
+
const text = await res.text().catch(() => '');
|
|
636
|
+
throw new AiBrokerError(
|
|
637
|
+
errorEnvelope(
|
|
638
|
+
res.status,
|
|
639
|
+
`openai engine: upstream ${res.status}: ${text.slice(0, 500)}`,
|
|
640
|
+
res.status === 404 ? 'NOT_FOUND' : 'INVALID_ARGUMENT',
|
|
641
|
+
),
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
return res;
|
|
645
|
+
}
|
|
646
|
+
}
|