pyric 0.1.0-alpha.19 → 0.1.0-alpha.21
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 +64 -89
- package/dist/ai/blocked.d.ts +52 -0
- package/dist/ai/blocked.d.ts.map +1 -0
- package/dist/ai/blocked.js +83 -0
- package/dist/ai/blocked.js.map +1 -0
- package/dist/ai/broker/broker.d.ts +50 -0
- package/dist/ai/broker/broker.d.ts.map +1 -1
- package/dist/ai/broker/broker.js +157 -21
- package/dist/ai/broker/broker.js.map +1 -1
- package/dist/ai/broker/gemini-engine.d.ts +56 -0
- package/dist/ai/broker/gemini-engine.d.ts.map +1 -0
- package/dist/ai/broker/gemini-engine.js +235 -0
- package/dist/ai/broker/gemini-engine.js.map +1 -0
- package/dist/ai/broker/index.d.ts +2 -1
- package/dist/ai/broker/index.d.ts.map +1 -1
- package/dist/ai/broker/index.js +2 -1
- package/dist/ai/broker/index.js.map +1 -1
- package/dist/ai/broker/openai-engine.d.ts +10 -1
- package/dist/ai/broker/openai-engine.d.ts.map +1 -1
- package/dist/ai/broker/openai-engine.js +26 -4
- package/dist/ai/broker/openai-engine.js.map +1 -1
- package/dist/ai/broker/synthesizer.d.ts +14 -0
- package/dist/ai/broker/synthesizer.d.ts.map +1 -1
- package/dist/ai/broker/synthesizer.js +31 -0
- package/dist/ai/broker/synthesizer.js.map +1 -1
- package/dist/ai/broker/types.d.ts +29 -0
- package/dist/ai/broker/types.d.ts.map +1 -1
- package/dist/ai/instances.js +1 -1
- package/dist/ai/instances.js.map +1 -1
- package/dist/ai/internal.d.ts +5 -0
- package/dist/ai/internal.d.ts.map +1 -1
- package/dist/ai/internal.js +5 -0
- package/dist/ai/internal.js.map +1 -1
- package/dist/ai/response-helpers.d.ts.map +1 -1
- package/dist/ai/response-helpers.js +6 -24
- package/dist/ai/response-helpers.js.map +1 -1
- package/dist/analytics/index.d.ts +40 -0
- package/dist/analytics/index.d.ts.map +1 -0
- package/dist/analytics/index.js +25 -0
- package/dist/analytics/index.js.map +1 -0
- package/dist/app-check/index.d.ts +31 -0
- package/dist/app-check/index.d.ts.map +1 -0
- package/dist/app-check/index.js +20 -0
- package/dist/app-check/index.js.map +1 -0
- package/dist/auth/instances.js +1 -1
- package/dist/auth/instances.js.map +1 -1
- package/dist/database/controls.d.ts +1 -1
- package/dist/database/controls.js +1 -1
- package/dist/database/instances.js +1 -1
- package/dist/database/instances.js.map +1 -1
- package/dist/database/sandbox/backend.d.ts +1 -0
- package/dist/database/sandbox/backend.d.ts.map +1 -1
- package/dist/database/sandbox/backend.js +1 -0
- package/dist/database/sandbox/backend.js.map +1 -1
- package/dist/database/sandbox/rules-eval.d.ts +6 -2
- package/dist/database/sandbox/rules-eval.d.ts.map +1 -1
- package/dist/database/sandbox/rules-eval.js +15 -2
- package/dist/database/sandbox/rules-eval.js.map +1 -1
- package/dist/database/sandbox/write-plane.d.ts +1 -0
- package/dist/database/sandbox/write-plane.d.ts.map +1 -1
- package/dist/database/sandbox/write-plane.js +7 -2
- package/dist/database/sandbox/write-plane.js.map +1 -1
- package/dist/database/sandbox-controls.d.ts +12 -5
- package/dist/database/sandbox-controls.d.ts.map +1 -1
- package/dist/database/sandbox-controls.js +20 -9
- package/dist/database/sandbox-controls.js.map +1 -1
- package/dist/database/sandbox-namespace.d.ts +19 -1
- package/dist/database/sandbox-namespace.d.ts.map +1 -1
- package/dist/database/sandbox-namespace.js +24 -1
- package/dist/database/sandbox-namespace.js.map +1 -1
- package/dist/deferred/entry.d.ts +75 -0
- package/dist/deferred/entry.d.ts.map +1 -0
- package/dist/deferred/entry.js +143 -0
- package/dist/deferred/entry.js.map +1 -0
- package/dist/firestore/instances.js +1 -1
- package/dist/firestore/instances.js.map +1 -1
- package/dist/firestore/lite.d.ts +72 -0
- package/dist/firestore/lite.d.ts.map +1 -0
- package/dist/firestore/lite.js +22 -0
- package/dist/firestore/lite.js.map +1 -0
- package/dist/firestore/persistence.d.ts +2 -2
- package/dist/firestore/persistence.js +1 -1
- package/dist/firestore/sandbox-controls.d.ts +2 -0
- package/dist/firestore/sandbox-controls.d.ts.map +1 -1
- package/dist/firestore/sandbox-controls.js +8 -0
- package/dist/firestore/sandbox-controls.js.map +1 -1
- package/dist/functions/index.d.ts +29 -0
- package/dist/functions/index.d.ts.map +1 -0
- package/dist/functions/index.js +20 -0
- package/dist/functions/index.js.map +1 -0
- package/dist/messaging/client.d.ts +2 -1
- package/dist/messaging/client.d.ts.map +1 -1
- package/dist/messaging/client.js +2 -2
- package/dist/messaging/client.js.map +1 -1
- package/dist/messaging/instance.d.ts +2 -2
- package/dist/messaging/instance.d.ts.map +1 -1
- package/dist/messaging/instance.js +40 -6
- package/dist/messaging/instance.js.map +1 -1
- package/dist/messaging/sw.d.ts +2 -1
- package/dist/messaging/sw.d.ts.map +1 -1
- package/dist/messaging/sw.js +2 -2
- package/dist/messaging/sw.js.map +1 -1
- package/dist/performance/index.d.ts +22 -0
- package/dist/performance/index.d.ts.map +1 -0
- package/dist/performance/index.js +19 -0
- package/dist/performance/index.js.map +1 -0
- package/dist/remote-config/index.d.ts +40 -0
- package/dist/remote-config/index.d.ts.map +1 -0
- package/dist/remote-config/index.js +25 -0
- package/dist/remote-config/index.js.map +1 -0
- package/dist/rules/grammar/FirestoreValidator.d.ts.map +1 -1
- package/dist/rules/grammar/FirestoreValidator.js +43 -27
- package/dist/rules/grammar/FirestoreValidator.js.map +1 -1
- package/dist/rules/grammar/document-access-count.d.ts +24 -0
- package/dist/rules/grammar/document-access-count.d.ts.map +1 -0
- package/dist/rules/grammar/document-access-count.js +100 -0
- package/dist/rules/grammar/document-access-count.js.map +1 -0
- package/dist/rules/linter/ast-utils.d.ts +2 -6
- package/dist/rules/linter/ast-utils.d.ts.map +1 -1
- package/dist/rules/linter/ast-utils.js +2 -63
- package/dist/rules/linter/ast-utils.js.map +1 -1
- package/dist/rules/linter/hallucinations.d.ts +8 -0
- package/dist/rules/linter/hallucinations.d.ts.map +1 -1
- package/dist/rules/linter/hallucinations.js +121 -31
- package/dist/rules/linter/hallucinations.js.map +1 -1
- package/dist/rules/linter/linter.d.ts +10 -1
- package/dist/rules/linter/linter.d.ts.map +1 -1
- package/dist/rules/linter/linter.js +23 -9
- package/dist/rules/linter/linter.js.map +1 -1
- package/dist/rules/rtdb/grammar/RtdbExpr.ohm +5 -2
- package/dist/rules/rtdb/grammar/RtdbExpr.ohm.generated.d.ts +1 -1
- package/dist/rules/rtdb/grammar/RtdbExpr.ohm.generated.d.ts.map +1 -1
- package/dist/rules/rtdb/grammar/RtdbExpr.ohm.generated.js +5 -2
- package/dist/rules/rtdb/grammar/RtdbExpr.ohm.generated.js.map +1 -1
- package/dist/rules/rtdb/grammar/simulator.d.ts.map +1 -1
- package/dist/rules/rtdb/grammar/simulator.js +61 -15
- package/dist/rules/rtdb/grammar/simulator.js.map +1 -1
- package/dist/rules/rtdb/simulation/handler.d.ts.map +1 -1
- package/dist/rules/rtdb/simulation/handler.js +96 -35
- package/dist/rules/rtdb/simulation/handler.js.map +1 -1
- package/dist/rules/simulator/document-lookups.d.ts +4 -0
- package/dist/rules/simulator/document-lookups.d.ts.map +1 -1
- package/dist/rules/simulator/document-lookups.js +42 -3
- package/dist/rules/simulator/document-lookups.js.map +1 -1
- package/dist/rules/simulator/eval-error.d.ts +11 -0
- package/dist/rules/simulator/eval-error.d.ts.map +1 -1
- package/dist/rules/simulator/eval-error.js +14 -0
- package/dist/rules/simulator/eval-error.js.map +1 -1
- package/dist/rules/simulator/evaluation-builtins.d.ts.map +1 -1
- package/dist/rules/simulator/evaluation-builtins.js +33 -8
- package/dist/rules/simulator/evaluation-builtins.js.map +1 -1
- package/dist/rules/simulator/evaluation-context.d.ts +12 -0
- package/dist/rules/simulator/evaluation-context.d.ts.map +1 -1
- package/dist/rules/simulator/evaluator.d.ts +1 -1
- package/dist/rules/simulator/evaluator.d.ts.map +1 -1
- package/dist/rules/simulator/evaluator.js +16 -4
- package/dist/rules/simulator/evaluator.js.map +1 -1
- package/dist/rules/simulator/handler.d.ts.map +1 -1
- package/dist/rules/simulator/handler.js +52 -5
- package/dist/rules/simulator/handler.js.map +1 -1
- package/dist/rules/simulator/lookup-budget.d.ts +37 -0
- package/dist/rules/simulator/lookup-budget.d.ts.map +1 -0
- package/dist/rules/simulator/lookup-budget.js +96 -0
- package/dist/rules/simulator/lookup-budget.js.map +1 -0
- package/dist/rules/stdlib-modules.d.ts +7 -0
- package/dist/rules/stdlib-modules.d.ts.map +1 -1
- package/dist/rules/stdlib-modules.js +11 -11
- package/dist/rules/stdlib-modules.js.map +1 -1
- package/dist/sandbox/admin-firestore/get-firestore.d.ts +6 -0
- package/dist/sandbox/admin-firestore/get-firestore.d.ts.map +1 -1
- package/dist/sandbox/admin-firestore/get-firestore.js +22 -3
- package/dist/sandbox/admin-firestore/get-firestore.js.map +1 -1
- package/dist/sandbox/index.d.ts +1 -1
- package/dist/sandbox/index.d.ts.map +1 -1
- package/dist/sandbox/index.js +1 -1
- package/dist/sandbox/index.js.map +1 -1
- package/dist/sandbox/persistence/types.d.ts +1 -1
- package/dist/sandbox/remote.d.ts +2 -2
- package/dist/sandbox/sandbox-context.d.ts +8 -1
- package/dist/sandbox/sandbox-context.d.ts.map +1 -1
- package/dist/sandbox/sandbox-context.js +59 -8
- package/dist/sandbox/sandbox-context.js.map +1 -1
- package/dist/sandbox/types/auth-state.d.ts +1 -0
- package/dist/sandbox/types/auth-state.d.ts.map +1 -1
- package/dist/sandbox/types/events.d.ts +2 -1
- package/dist/sandbox/types/events.d.ts.map +1 -1
- package/dist/sandbox/types/operation.d.ts +1 -0
- package/dist/sandbox/types/operation.d.ts.map +1 -1
- package/dist/storage/enforce.d.ts +20 -1
- package/dist/storage/enforce.d.ts.map +1 -1
- package/dist/storage/enforce.js +47 -14
- package/dist/storage/enforce.js.map +1 -1
- package/dist/storage/index.d.ts +1 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/list.js +1 -1
- package/dist/storage/list.js.map +1 -1
- package/dist/storage/persistence.d.ts.map +1 -1
- package/dist/storage/persistence.js +66 -14
- package/dist/storage/persistence.js.map +1 -1
- package/dist/storage/sandbox/rules-evaluation-error.d.ts +25 -0
- package/dist/storage/sandbox/rules-evaluation-error.d.ts.map +1 -1
- package/dist/storage/sandbox/rules-evaluation-error.js +29 -0
- package/dist/storage/sandbox/rules-evaluation-error.js.map +1 -1
- package/dist/storage/sandbox/rules-evaluator.d.ts +10 -5
- package/dist/storage/sandbox/rules-evaluator.d.ts.map +1 -1
- package/dist/storage/sandbox/rules-evaluator.js +105 -33
- package/dist/storage/sandbox/rules-evaluator.js.map +1 -1
- package/dist/storage/sandbox/rules-methods.d.ts.map +1 -1
- package/dist/storage/sandbox/rules-methods.js +17 -7
- package/dist/storage/sandbox/rules-methods.js.map +1 -1
- package/dist/storage/service.d.ts +23 -2
- package/dist/storage/service.d.ts.map +1 -1
- package/dist/storage/service.js +37 -26
- package/dist/storage/service.js.map +1 -1
- package/package.json +42 -2
- package/src/ai/blocked.ts +97 -0
- package/src/ai/broker/broker.ts +153 -22
- package/src/ai/broker/gemini-engine.ts +311 -0
- package/src/ai/broker/index.ts +2 -0
- package/src/ai/broker/openai-engine.ts +31 -3
- package/src/ai/broker/synthesizer.ts +34 -0
- package/src/ai/broker/types.ts +27 -1
- package/src/ai/instances.ts +1 -1
- package/src/ai/internal.ts +5 -0
- package/src/ai/response-helpers.ts +6 -25
- package/src/analytics/index.ts +53 -0
- package/src/app/dispatch.test.ts +3 -1
- package/src/app-check/index.ts +41 -0
- package/src/auth/instances.ts +1 -1
- package/src/database/controls.ts +1 -1
- package/src/database/instances.ts +1 -1
- package/src/database/sandbox/backend.ts +1 -0
- package/src/database/sandbox/rules-eval.ts +18 -2
- package/src/database/sandbox/write-plane.ts +8 -2
- package/src/database/sandbox-controls.ts +29 -9
- package/src/database/sandbox-namespace.ts +28 -1
- package/src/deferred/entry.ts +167 -0
- package/src/firestore/instances.ts +1 -1
- package/src/firestore/lite.ts +91 -0
- package/src/firestore/persistence.ts +2 -2
- package/src/firestore/sandbox-controls.ts +10 -0
- package/src/functions/index.ts +38 -0
- package/src/messaging/client.ts +3 -2
- package/src/messaging/instance.ts +44 -7
- package/src/messaging/sw.ts +3 -2
- package/src/performance/index.ts +31 -0
- package/src/remote-config/index.ts +53 -0
- package/src/rules/grammar/FirestoreValidator.ts +44 -28
- package/src/rules/grammar/document-access-count.ts +75 -0
- package/src/rules/linter/ast-utils.ts +2 -36
- package/src/rules/linter/hallucinations.ts +123 -25
- package/src/rules/linter/linter.ts +32 -10
- package/src/rules/rtdb/grammar/RtdbExpr.ohm +5 -2
- package/src/rules/rtdb/grammar/RtdbExpr.ohm.generated.ts +5 -2
- package/src/rules/rtdb/grammar/simulator.ts +41 -15
- package/src/rules/rtdb/simulation/handler.ts +149 -46
- package/src/rules/simulator/document-lookups.ts +46 -3
- package/src/rules/simulator/eval-error.ts +16 -0
- package/src/rules/simulator/evaluation-builtins.ts +35 -8
- package/src/rules/simulator/evaluation-context.ts +12 -0
- package/src/rules/simulator/evaluator.ts +14 -3
- package/src/rules/simulator/handler.ts +68 -4
- package/src/rules/simulator/lookup-budget.ts +101 -0
- package/src/rules/stdlib-modules.ts +19 -11
- package/src/sandbox/admin-firestore/get-firestore.ts +19 -3
- package/src/sandbox/index.ts +5 -1
- package/src/sandbox/persistence/types.ts +1 -1
- package/src/sandbox/remote.ts +2 -2
- package/src/sandbox/sandbox-context.ts +69 -9
- package/src/sandbox/types/auth-state.ts +1 -1
- package/src/sandbox/types/events.ts +2 -1
- package/src/sandbox/types/operation.ts +1 -1
- package/src/storage/enforce.ts +53 -14
- package/src/storage/index.ts +1 -1
- package/src/storage/list.ts +1 -1
- package/src/storage/persistence.ts +74 -16
- package/src/storage/sandbox/rules-evaluation-error.ts +32 -0
- package/src/storage/sandbox/rules-evaluator.ts +109 -32
- package/src/storage/sandbox/rules-methods.ts +21 -7
- package/src/storage/service.ts +93 -27
- package/README.md.orig +0 -53
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Gemini AnswerEngine: Gemini wire in, Google AI Studio / Vertex AI REST API
|
|
3
|
+
* out, Gemini wire back.
|
|
4
|
+
*
|
|
5
|
+
* Unlike the browser-side `@firebase/ai` SDK (which calls Vertex AI directly
|
|
6
|
+
* from the browser and fails with HTTP 401 for sandboxed mock auth users),
|
|
7
|
+
* `GeminiEngine` runs on Pyric's backend server, authenticating via a local
|
|
8
|
+
* API key (`GEMINI_API_KEY` / `GOOGLE_GENAI_API_KEY` / `VITE_GEMINI_API_KEY`)
|
|
9
|
+
* or Google Cloud Application Default Credentials (`gcloud auth application-default login`)
|
|
10
|
+
* without exposing secrets to the browser.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { AiBrokerError, errorEnvelope, redactUrl } from './synthesizer.js';
|
|
14
|
+
import type {
|
|
15
|
+
AnswerEngine,
|
|
16
|
+
CountTokensRequest,
|
|
17
|
+
CountTokensResponse,
|
|
18
|
+
GenerateContentRequest,
|
|
19
|
+
ModelResolution,
|
|
20
|
+
WireChunk,
|
|
21
|
+
WireResponse,
|
|
22
|
+
} from './types.js';
|
|
23
|
+
|
|
24
|
+
/** Where the gemini engine talks when no `baseUrl` overrides it. Published
|
|
25
|
+
* through `pyric/ai/internal` so the CLI's startup banner can name the same
|
|
26
|
+
* endpoint instead of keeping its own copy. */
|
|
27
|
+
export const GEMINI_DEFAULT_BASE_URL = 'https://generativelanguage.googleapis.com';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* BROWSER-CLEAN: acquires `node:child_process` lazily via `process.getBuiltinModule`
|
|
31
|
+
* so no static node import exists for browser bundlers to resolve.
|
|
32
|
+
*/
|
|
33
|
+
function getExecSync(): ((command: string, options: Record<string, unknown>) => string) | null {
|
|
34
|
+
const get = (globalThis as { process?: { getBuiltinModule?: (id: string) => unknown } }).process
|
|
35
|
+
?.getBuiltinModule;
|
|
36
|
+
const hasGetBuiltinModule = typeof get === 'function';
|
|
37
|
+
if (!hasGetBuiltinModule) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const cp = get.call(process, 'node:child_process') as
|
|
41
|
+
| { execSync?: (cmd: string, opts: unknown) => string }
|
|
42
|
+
| undefined;
|
|
43
|
+
const hasExecSync = cp !== undefined && typeof cp.execSync === 'function';
|
|
44
|
+
if (!hasExecSync) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return cp.execSync as (command: string, options: Record<string, unknown>) => string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface GeminiEngineOptions {
|
|
51
|
+
/** Explicit API key. Absent ⇒ resolved from environment variables or ADC. */
|
|
52
|
+
apiKey?: string;
|
|
53
|
+
/** Explicit base URL override (default: https://generativelanguage.googleapis.com). */
|
|
54
|
+
baseUrl?: string;
|
|
55
|
+
/** Explicit fetch seam for testing or custom transport. */
|
|
56
|
+
fetch?: typeof fetch;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class GeminiEngine implements AnswerEngine {
|
|
60
|
+
private readonly baseUrl: string;
|
|
61
|
+
private readonly fetchImpl: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
62
|
+
private readonly explicitKey?: string;
|
|
63
|
+
|
|
64
|
+
constructor(options?: GeminiEngineOptions) {
|
|
65
|
+
this.baseUrl = (options?.baseUrl ?? GEMINI_DEFAULT_BASE_URL).replace(/\/$/, '');
|
|
66
|
+
this.fetchImpl = options?.fetch ?? ((input, init) => fetch(input, init));
|
|
67
|
+
this.explicitKey = options?.apiKey;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resolves authentication credentials sequentially, checking static options,
|
|
72
|
+
* environment variables, and finally Google Cloud Application Default
|
|
73
|
+
* Credentials (`gcloud auth application-default print-access-token`).
|
|
74
|
+
*/
|
|
75
|
+
private async resolveAuthToken(): Promise<{ token: string; isBearer: boolean }> {
|
|
76
|
+
let key = this.explicitKey;
|
|
77
|
+
if (key === undefined) {
|
|
78
|
+
key = process.env.GEMINI_API_KEY;
|
|
79
|
+
}
|
|
80
|
+
if (key === undefined) {
|
|
81
|
+
key = process.env.GOOGLE_GENAI_API_KEY;
|
|
82
|
+
}
|
|
83
|
+
if (key === undefined) {
|
|
84
|
+
key = process.env.VITE_GEMINI_API_KEY;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (key !== undefined) {
|
|
88
|
+
const trimmedKey = key.trim();
|
|
89
|
+
const hasStaticKey = trimmedKey !== '';
|
|
90
|
+
if (hasStaticKey) {
|
|
91
|
+
return { token: trimmedKey, isBearer: false };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const execSyncFn = getExecSync();
|
|
97
|
+
const hasExecSyncFn = execSyncFn !== null;
|
|
98
|
+
if (hasExecSyncFn) {
|
|
99
|
+
const adcToken = execSyncFn('gcloud auth application-default print-access-token', {
|
|
100
|
+
encoding: 'utf8',
|
|
101
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
102
|
+
}).trim();
|
|
103
|
+
const hasAdcToken = adcToken !== '';
|
|
104
|
+
if (hasAdcToken) {
|
|
105
|
+
return { token: adcToken, isBearer: true };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
// Best-effort attempt to recover ADC credentials when env keys are absent
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
throw new AiBrokerError(
|
|
113
|
+
errorEnvelope(
|
|
114
|
+
401,
|
|
115
|
+
'Pyric AI production passthrough mode requires GEMINI_API_KEY, GOOGLE_GENAI_API_KEY, or VITE_GEMINI_API_KEY in your server environment (or Application Default Credentials via `gcloud auth application-default login`).',
|
|
116
|
+
'UNAUTHENTICATED',
|
|
117
|
+
),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Which model this engine ACTUALLY calls upstream, and why it differs.
|
|
123
|
+
* Experimental and superseded flash aliases are redirected onto the served
|
|
124
|
+
* `gemini-flash-lite-latest`; everything else passes through. Reported so
|
|
125
|
+
* the broker can announce the redirect rather than let a developer believe
|
|
126
|
+
* `gemini-2.5-flash` answered when `gemini-flash-lite-latest` did
|
|
127
|
+
* ({@link AnswerEngine.resolveEffectiveModel}).
|
|
128
|
+
*/
|
|
129
|
+
resolveEffectiveModel(model: string): ModelResolution {
|
|
130
|
+
const stripped = model.replace(/^models\//, '');
|
|
131
|
+
const isExperimentalFlashLite =
|
|
132
|
+
stripped === 'gemini-3.5-flash-lite' ||
|
|
133
|
+
stripped === 'gemini-2.5-flash' ||
|
|
134
|
+
stripped === 'gemini-2.5-flash-lite' ||
|
|
135
|
+
stripped === 'gemini-1.5-flash';
|
|
136
|
+
if (isExperimentalFlashLite) {
|
|
137
|
+
return { model: 'gemini-flash-lite-latest', reason: 'experimental alias' };
|
|
138
|
+
}
|
|
139
|
+
return { model: stripped, reason: 'passthrough' };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private normalizeModel(model: string): string {
|
|
143
|
+
return `models/${this.resolveEffectiveModel(model).model}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Formats upstream REST endpoint URLs and request headers for either Google
|
|
148
|
+
* AI Studio (`generativelanguage.googleapis.com`) or Vertex AI regional APIs.
|
|
149
|
+
*/
|
|
150
|
+
private formatUpstreamRequest(
|
|
151
|
+
resource: string,
|
|
152
|
+
action: string,
|
|
153
|
+
auth: { token: string; isBearer: boolean },
|
|
154
|
+
): { url: string; headers: Record<string, string> } {
|
|
155
|
+
const isVertexUrl =
|
|
156
|
+
this.baseUrl.includes('firebasevertexai.googleapis.com') ||
|
|
157
|
+
this.baseUrl.includes('aiplatform.googleapis.com');
|
|
158
|
+
const shouldUseBearerAuth = auth.isBearer || isVertexUrl;
|
|
159
|
+
|
|
160
|
+
const headers: Record<string, string> = {
|
|
161
|
+
'Content-Type': 'application/json',
|
|
162
|
+
};
|
|
163
|
+
if (shouldUseBearerAuth) {
|
|
164
|
+
headers['Authorization'] = `Bearer ${auth.token}`;
|
|
165
|
+
return {
|
|
166
|
+
url: `${this.baseUrl}/v1beta/${resource}:${action}`,
|
|
167
|
+
headers,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
url: `${this.baseUrl}/v1beta/${resource}:${action}?key=${encodeURIComponent(auth.token)}`,
|
|
173
|
+
headers,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private async fetchUpstream(
|
|
178
|
+
url: string,
|
|
179
|
+
headers: Record<string, string>,
|
|
180
|
+
body: unknown,
|
|
181
|
+
): Promise<Response> {
|
|
182
|
+
let response: Response;
|
|
183
|
+
try {
|
|
184
|
+
response = await this.fetchImpl(url, {
|
|
185
|
+
method: 'POST',
|
|
186
|
+
headers,
|
|
187
|
+
body: JSON.stringify(body),
|
|
188
|
+
});
|
|
189
|
+
} catch (err) {
|
|
190
|
+
// Redact both: `url` always carries the key when auth is a static key
|
|
191
|
+
// (not bearer/ADC); `message` is defensively redacted too in case the
|
|
192
|
+
// fetch implementation's own error text echoes the request URL.
|
|
193
|
+
const rawMessage = err instanceof Error ? err.message : String(err);
|
|
194
|
+
const safeUrl = redactUrl(url);
|
|
195
|
+
const safeMessage = redactUrl(rawMessage);
|
|
196
|
+
throw new AiBrokerError(
|
|
197
|
+
errorEnvelope(
|
|
198
|
+
502,
|
|
199
|
+
`Failed to connect to Gemini API at ${safeUrl}: ${safeMessage}`,
|
|
200
|
+
'UNAVAILABLE',
|
|
201
|
+
),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const isSuccess = response.ok;
|
|
206
|
+
if (!isSuccess) {
|
|
207
|
+
const errorText = await response.text().catch(() => '');
|
|
208
|
+
throw new AiBrokerError(
|
|
209
|
+
errorEnvelope(
|
|
210
|
+
response.status,
|
|
211
|
+
`Gemini API returned status ${response.status}: ${redactUrl(errorText)}`,
|
|
212
|
+
'INTERNAL',
|
|
213
|
+
),
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return response;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async generateContent(req: GenerateContentRequest, model: string): Promise<WireResponse> {
|
|
221
|
+
const auth = await this.resolveAuthToken();
|
|
222
|
+
const resource = this.normalizeModel(model);
|
|
223
|
+
const { url, headers } = this.formatUpstreamRequest(resource, 'generateContent', auth);
|
|
224
|
+
const response = await this.fetchUpstream(url, headers, req);
|
|
225
|
+
return (await response.json()) as WireResponse;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async *streamGenerateContent(
|
|
229
|
+
req: GenerateContentRequest,
|
|
230
|
+
model: string,
|
|
231
|
+
): AsyncIterable<WireChunk> {
|
|
232
|
+
const auth = await this.resolveAuthToken();
|
|
233
|
+
const resource = this.normalizeModel(model);
|
|
234
|
+
const { url, headers } = this.formatUpstreamRequest(
|
|
235
|
+
resource,
|
|
236
|
+
'streamGenerateContent?alt=sse',
|
|
237
|
+
auth,
|
|
238
|
+
);
|
|
239
|
+
const response = await this.fetchUpstream(url, headers, req);
|
|
240
|
+
|
|
241
|
+
const hasBody = response.body !== null && response.body !== undefined;
|
|
242
|
+
if (!hasBody) {
|
|
243
|
+
throw new AiBrokerError(
|
|
244
|
+
errorEnvelope(500, 'Gemini API returned an empty stream response.', 'INTERNAL'),
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const reader = response.body.getReader();
|
|
249
|
+
const decoder = new TextDecoder();
|
|
250
|
+
let buffer = '';
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
while (true) {
|
|
254
|
+
const { done, value } = await reader.read();
|
|
255
|
+
const isDone = done === true;
|
|
256
|
+
if (isDone) {
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
buffer += decoder.decode(value, { stream: true });
|
|
260
|
+
const lines = buffer.split(/\r?\n/);
|
|
261
|
+
buffer = lines.pop() ?? '';
|
|
262
|
+
|
|
263
|
+
for (const line of lines) {
|
|
264
|
+
const isDataLine = line.startsWith('data: ');
|
|
265
|
+
if (!isDataLine) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const dataStr = line.slice(6).trim();
|
|
269
|
+
const isEmptyOrDone = dataStr === '' || dataStr === '[DONE]';
|
|
270
|
+
if (isEmptyOrDone) {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
const chunk = JSON.parse(dataStr) as WireChunk;
|
|
275
|
+
yield chunk;
|
|
276
|
+
} catch {
|
|
277
|
+
// Ignore malformed SSE chunk in stream
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const hasTrailingBuffer = buffer.trim().length > 0;
|
|
283
|
+
if (hasTrailingBuffer) {
|
|
284
|
+
const line = buffer.trim();
|
|
285
|
+
const isDataLine = line.startsWith('data: ');
|
|
286
|
+
if (isDataLine) {
|
|
287
|
+
const dataStr = line.slice(6).trim();
|
|
288
|
+
const isNotEmptyOrDone = dataStr !== '' && dataStr !== '[DONE]';
|
|
289
|
+
if (isNotEmptyOrDone) {
|
|
290
|
+
try {
|
|
291
|
+
const chunk = JSON.parse(dataStr) as WireChunk;
|
|
292
|
+
yield chunk;
|
|
293
|
+
} catch {
|
|
294
|
+
// Ignore malformed SSE chunk in stream
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
} finally {
|
|
300
|
+
reader.releaseLock();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async countTokens(req: CountTokensRequest, model: string): Promise<CountTokensResponse> {
|
|
305
|
+
const auth = await this.resolveAuthToken();
|
|
306
|
+
const resource = this.normalizeModel(model);
|
|
307
|
+
const { url, headers } = this.formatUpstreamRequest(resource, 'countTokens', auth);
|
|
308
|
+
const response = await this.fetchUpstream(url, headers, req);
|
|
309
|
+
return (await response.json()) as CountTokensResponse;
|
|
310
|
+
}
|
|
311
|
+
}
|
package/src/ai/broker/index.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
export { AiBroker, loadObservationEnvelope, type AiBrokerOptions } from './broker.js';
|
|
9
9
|
export { ScriptedEngine, lastUserText, promptTextOf } from './scripted-engine.js';
|
|
10
|
+
export { GeminiEngine, GEMINI_DEFAULT_BASE_URL, type GeminiEngineOptions } from './gemini-engine.js';
|
|
10
11
|
export {
|
|
11
12
|
OpenAiEngine,
|
|
12
13
|
ToolCallBuffer,
|
|
@@ -36,6 +37,7 @@ export {
|
|
|
36
37
|
estimateTokens,
|
|
37
38
|
mintThoughtSignature,
|
|
38
39
|
mintFunctionCallId,
|
|
40
|
+
redactUrl,
|
|
39
41
|
type SynthesizeOptions,
|
|
40
42
|
} from './synthesizer.js';
|
|
41
43
|
export type {
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
Synthesizer,
|
|
30
30
|
errorEnvelope,
|
|
31
31
|
estimateTokens,
|
|
32
|
+
redactUrl,
|
|
32
33
|
resolveModelVersion,
|
|
33
34
|
} from './synthesizer.js';
|
|
34
35
|
import { promptTextOf } from './scripted-engine.js';
|
|
@@ -37,6 +38,7 @@ import type {
|
|
|
37
38
|
CountTokensRequest,
|
|
38
39
|
CountTokensResponse,
|
|
39
40
|
GenerateContentRequest,
|
|
41
|
+
ModelResolution,
|
|
40
42
|
WireChunk,
|
|
41
43
|
WirePart,
|
|
42
44
|
WireResponse,
|
|
@@ -487,10 +489,27 @@ export class OpenAiEngine implements AnswerEngine {
|
|
|
487
489
|
);
|
|
488
490
|
}
|
|
489
491
|
|
|
492
|
+
/**
|
|
493
|
+
* Gemini model id to upstream model, WITH the reason: `modelMap[model]` ??
|
|
494
|
+
* `config.model` ?? passthrough. The reason is what makes the redirect
|
|
495
|
+
* legible in the dev terminal: "which of my three knobs sent this
|
|
496
|
+
* elsewhere?" is the whole question when the answer came from a model the
|
|
497
|
+
* developer never named. Bare (never `models/`-prefixed), per
|
|
498
|
+
* {@link AnswerEngine.resolveEffectiveModel}.
|
|
499
|
+
*/
|
|
500
|
+
resolveEffectiveModel(model: string): ModelResolution {
|
|
501
|
+
const bare = model.startsWith('models/') ? model.slice('models/'.length) : model;
|
|
502
|
+
const mapped = this.modelMap[bare] ?? this.modelMap[model];
|
|
503
|
+
if (mapped !== undefined) return { model: mapped, reason: 'engine modelMap' };
|
|
504
|
+
if (this.defaultModel !== undefined) {
|
|
505
|
+
return { model: this.defaultModel, reason: 'engine catch-all model' };
|
|
506
|
+
}
|
|
507
|
+
return { model: bare, reason: 'passthrough' };
|
|
508
|
+
}
|
|
509
|
+
|
|
490
510
|
/** Gemini model id → upstream model: modelMap[model] ?? config.model ?? passthrough. */
|
|
491
511
|
resolveUpstreamModel(model: string): string {
|
|
492
|
-
|
|
493
|
-
return this.modelMap[bare] ?? this.modelMap[model] ?? this.defaultModel ?? bare;
|
|
512
|
+
return this.resolveEffectiveModel(model).model;
|
|
494
513
|
}
|
|
495
514
|
|
|
496
515
|
async generateContent(req: GenerateContentRequest, model: string): Promise<WireResponse> {
|
|
@@ -627,8 +646,17 @@ export class OpenAiEngine implements AnswerEngine {
|
|
|
627
646
|
body: JSON.stringify(body),
|
|
628
647
|
});
|
|
629
648
|
} catch (err) {
|
|
649
|
+
// The openai engine has no key-in-URL auth mechanism today
|
|
650
|
+
// (`OpenAiEngineOptions` carries no `apiKey`), but a caller-supplied
|
|
651
|
+
// `baseUrl` could embed a credential in its query string, and some
|
|
652
|
+
// fetch implementations echo the request URL into their error text —
|
|
653
|
+
// redact defensively at this choke point (T1.7).
|
|
630
654
|
throw new AiBrokerError(
|
|
631
|
-
errorEnvelope(
|
|
655
|
+
errorEnvelope(
|
|
656
|
+
502,
|
|
657
|
+
`openai engine: upstream fetch failed: ${redactUrl(String(err))}`,
|
|
658
|
+
'UNAVAILABLE',
|
|
659
|
+
),
|
|
632
660
|
);
|
|
633
661
|
}
|
|
634
662
|
if (!res.ok) {
|
|
@@ -117,6 +117,40 @@ export function errorEnvelope(code: number, message: string, status: string): Wi
|
|
|
117
117
|
return { error: { code, message, status } };
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
// ── URL redaction (T1.7) ─────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Query-param names that carry plaintext credentials on upstream REST
|
|
124
|
+
* requests (Google AI Studio's `?key=...` auth is the known case; the rest
|
|
125
|
+
* are defensive for sibling engines / future upstreams).
|
|
126
|
+
*
|
|
127
|
+
* Matched with a direct regex over the raw string rather than
|
|
128
|
+
* `new URL(...).searchParams` — the streaming action string embeds its own
|
|
129
|
+
* `?alt=sse` before the `?key=` is appended (a pre-existing, unrelated
|
|
130
|
+
* quirk this fix does not touch), which the strict `URLSearchParams` parser
|
|
131
|
+
* would fold into a single malformed pair and fail to redact. A regex on
|
|
132
|
+
* "`[?&]<param>=`" catches the value regardless of how the surrounding
|
|
133
|
+
* query string is (mal)formed.
|
|
134
|
+
*/
|
|
135
|
+
const SENSITIVE_URL_PARAM_PATTERN = /([?&](?:key|apiKey|api_key|access_token)=)[^&\s]*/gi;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Redacts credential-bearing query-string VALUES from a URL (or any string
|
|
139
|
+
* that may embed one) before it lands in an error message or log line —
|
|
140
|
+
* e.g. `?key=AIza...` becomes `?key=***`. Host and path are preserved so
|
|
141
|
+
* the message stays useful for diagnosing connectivity failures; only the
|
|
142
|
+
* secret value is masked.
|
|
143
|
+
*
|
|
144
|
+
* Used at every choke point where an upstream request URL is interpolated
|
|
145
|
+
* into a thrown error or logged text, so a leaked key never reaches
|
|
146
|
+
* terminal output, CI logs, or Studio traffic captures. Safe to apply
|
|
147
|
+
* defensively to values that are not themselves URLs (e.g. a raw fetch
|
|
148
|
+
* error message) — it is a no-op when no sensitive param is present.
|
|
149
|
+
*/
|
|
150
|
+
export function redactUrl(url: string): string {
|
|
151
|
+
return url.replace(SENSITIVE_URL_PARAM_PATTERN, '$1***');
|
|
152
|
+
}
|
|
153
|
+
|
|
120
154
|
/** `ai-error-unknown-model` (404 NOT_FOUND), captured text verbatim — including production's `v1main`. */
|
|
121
155
|
export function unknownModel(name: string): WireErrorEnvelope {
|
|
122
156
|
return errorEnvelope(
|
package/src/ai/broker/types.ts
CHANGED
|
@@ -136,6 +136,31 @@ export interface AnswerEngine {
|
|
|
136
136
|
generateContent(req: GenerateContentRequest, model: string): Promise<WireResponse>;
|
|
137
137
|
streamGenerateContent(req: GenerateContentRequest, model: string): AsyncIterable<WireChunk>;
|
|
138
138
|
countTokens(req: CountTokensRequest, model: string): Promise<CountTokensResponse>;
|
|
139
|
+
/**
|
|
140
|
+
* OPTIONAL self-report: which model this engine will ACTUALLY answer with
|
|
141
|
+
* for a requested Gemini model id, and why it differs. Engines that redirect
|
|
142
|
+
* silently (an openai `modelMap` entry or catch-all `model`, a gemini
|
|
143
|
+
* experimental alias) implement it so the broker can announce the swap on
|
|
144
|
+
* the event stream. A developer must never believe they tested model X when
|
|
145
|
+
* model Y answered.
|
|
146
|
+
*
|
|
147
|
+
* Engines that never substitute (scripted, a caller's custom engine) omit
|
|
148
|
+
* the method entirely, and the broker stays silent for them.
|
|
149
|
+
*/
|
|
150
|
+
resolveEffectiveModel?(model: string): ModelResolution;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Which model an engine answers with, and why it differs from the one the
|
|
155
|
+
* request named.
|
|
156
|
+
*
|
|
157
|
+
* `model` is BARE (no `models/` prefix) so the broker can compare it against
|
|
158
|
+
* the requested id without the prefix reading as a substitution. `reason` is
|
|
159
|
+
* a short phrase for the terminal line, e.g. `engine modelMap`.
|
|
160
|
+
*/
|
|
161
|
+
export interface ModelResolution {
|
|
162
|
+
model: string;
|
|
163
|
+
reason: string;
|
|
139
164
|
}
|
|
140
165
|
|
|
141
166
|
// ── Scripted authoring ──────────────────────────────────────────────────────
|
|
@@ -191,4 +216,5 @@ export type EngineConfig =
|
|
|
191
216
|
modelMap?: Record<string, string>;
|
|
192
217
|
/** Injectable fetch for tests; defaults to globalThis.fetch. */
|
|
193
218
|
fetch?: typeof fetch;
|
|
194
|
-
}
|
|
219
|
+
}
|
|
220
|
+
| { kind: 'gemini'; baseUrl?: string; apiKey?: string; fetch?: typeof fetch };
|
package/src/ai/instances.ts
CHANGED
|
@@ -91,7 +91,7 @@ export function getAI(target?: Sandbox | FirebaseApp, options?: AIOptions): AI {
|
|
|
91
91
|
return sandboxAI(target, options);
|
|
92
92
|
}
|
|
93
93
|
throw new TypeError(
|
|
94
|
-
'pyric/ai is a sandbox-only mirror. Package resolution must leave firebase/ai unchanged for production; activate pyric
|
|
94
|
+
'pyric/ai is a sandbox-only mirror. Package resolution must leave firebase/ai unchanged for production; activate pyric sandbox or @pyric/cli/register before importing to select the sandbox.',
|
|
95
95
|
);
|
|
96
96
|
}
|
|
97
97
|
|
package/src/ai/internal.ts
CHANGED
|
@@ -6,6 +6,11 @@ import { TARGET_SYMBOL } from './target.js';
|
|
|
6
6
|
import type { AI, AIOptions, TransportTarget } from './types.js';
|
|
7
7
|
|
|
8
8
|
export { aiErrorFromEnvelope } from './errors.js';
|
|
9
|
+
/** Host-only seam for the dev server's terminal diagnostics: the credential
|
|
10
|
+
* masking every printed upstream URL goes through, and the gemini endpoint
|
|
11
|
+
* the startup banner names when no `baseUrl` overrides it. Published here so
|
|
12
|
+
* `@pyric/cli` reads one definition instead of copying both. */
|
|
13
|
+
export { GEMINI_DEFAULT_BASE_URL, redactUrl } from './broker/index.js';
|
|
9
14
|
|
|
10
15
|
const transportHandles = new WeakMap<FirebaseApp, Map<string, AI>>();
|
|
11
16
|
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import type { WireCandidate, WirePart, WireResponse } from './broker/index.js';
|
|
23
23
|
import { AIError, AIErrorCode } from './errors.js';
|
|
24
|
-
import {
|
|
24
|
+
import { isBlockingFinishReason } from './blocked.js';
|
|
25
25
|
|
|
26
26
|
/** The wire envelope plus the SDK's helper methods. */
|
|
27
27
|
export interface EnhancedResponse extends WireResponse {
|
|
@@ -31,28 +31,9 @@ export interface EnhancedResponse extends WireResponse {
|
|
|
31
31
|
functionCalls(): Array<NonNullable<WirePart['functionCall']>> | undefined;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
FinishReason.BLOCKLIST,
|
|
38
|
-
FinishReason.PROHIBITED_CONTENT,
|
|
39
|
-
FinishReason.SPII,
|
|
40
|
-
FinishReason.MALFORMED_FUNCTION_CALL,
|
|
41
|
-
FinishReason.IMAGE_SAFETY,
|
|
42
|
-
FinishReason.IMAGE_PROHIBITED_CONTENT,
|
|
43
|
-
FinishReason.IMAGE_OTHER,
|
|
44
|
-
FinishReason.NO_IMAGE,
|
|
45
|
-
FinishReason.IMAGE_RECITATION,
|
|
46
|
-
FinishReason.LANGUAGE,
|
|
47
|
-
FinishReason.UNEXPECTED_TOOL_CALL,
|
|
48
|
-
FinishReason.TOO_MANY_TOOL_CALLS,
|
|
49
|
-
FinishReason.MISSING_THOUGHT_SIGNATURE,
|
|
50
|
-
FinishReason.MALFORMED_RESPONSE,
|
|
51
|
-
];
|
|
52
|
-
|
|
53
|
-
function hadBadFinishReason(candidate: WireCandidate): boolean {
|
|
54
|
-
return !!candidate.finishReason && badFinishReasons.some((r) => r === candidate.finishReason);
|
|
55
|
-
}
|
|
34
|
+
// Upstream's `badFinishReasons` set lives in `blocked.ts`: the broker
|
|
35
|
+
// announces the SAME set on the event stream (`response_blocked`), and the
|
|
36
|
+
// two must never drift apart.
|
|
56
37
|
|
|
57
38
|
export function formatBlockErrorMessage(response: WireResponse): string {
|
|
58
39
|
let message = '';
|
|
@@ -63,7 +44,7 @@ export function formatBlockErrorMessage(response: WireResponse): string {
|
|
|
63
44
|
}
|
|
64
45
|
} else if (response.candidates?.[0]) {
|
|
65
46
|
const firstCandidate = response.candidates[0];
|
|
66
|
-
if (
|
|
47
|
+
if (isBlockingFinishReason(firstCandidate.finishReason)) {
|
|
67
48
|
message += `Candidate was blocked due to ${firstCandidate.finishReason}`;
|
|
68
49
|
if (firstCandidate.finishMessage) {
|
|
69
50
|
message += `: ${firstCandidate.finishMessage}`;
|
|
@@ -76,7 +57,7 @@ export function formatBlockErrorMessage(response: WireResponse): string {
|
|
|
76
57
|
/** At least one candidate exists and the first has no bad finish reason. */
|
|
77
58
|
function hasValidCandidates(response: WireResponse): boolean {
|
|
78
59
|
if (response.candidates && response.candidates.length > 0) {
|
|
79
|
-
if (
|
|
60
|
+
if (isBlockingFinishReason(response.candidates[0]!.finishReason)) {
|
|
80
61
|
throw new AIError(
|
|
81
62
|
AIErrorCode.RESPONSE_ERROR,
|
|
82
63
|
`Response error: ${formatBlockErrorMessage(response)}. Response body stored in error.response`,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pyric/analytics` — a DEFERRED mirror of `firebase/analytics`.
|
|
3
|
+
*
|
|
4
|
+
* Google Analytics for Firebase is a fire-and-forget telemetry pipe to
|
|
5
|
+
* Google servers. There is no observable local behavior for the sandbox to
|
|
6
|
+
* mirror, so it is deferred.
|
|
7
|
+
*
|
|
8
|
+
* Every symbol below resolves and links so an app that swaps `firebase` for
|
|
9
|
+
* `pyric` still loads; touching one throws `PyricDeferredApiError` naming this
|
|
10
|
+
* subpath. See `../deferred/entry.ts` for the full rationale.
|
|
11
|
+
*
|
|
12
|
+
* The value list is the exact public runtime surface of `firebase/analytics`
|
|
13
|
+
* (Firebase Web SDK 12.13.0). Keep it in sync when this entry graduates to a
|
|
14
|
+
* real mirror.
|
|
15
|
+
*/
|
|
16
|
+
import { deferredEntry, type DeferredApi } from '../deferred/entry.js';
|
|
17
|
+
|
|
18
|
+
export { PyricDeferredApiError } from '../deferred/entry.js';
|
|
19
|
+
|
|
20
|
+
export const {
|
|
21
|
+
getAnalytics, getGoogleAnalyticsClientId, initializeAnalytics, logEvent,
|
|
22
|
+
setAnalyticsCollectionEnabled, setConsent, setCurrentScreen, setDefaultEventParameters,
|
|
23
|
+
setUserId, setUserProperties, settings,
|
|
24
|
+
} = deferredEntry('analytics');
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The one deferred symbol that answers instead of throwing: the real SDK
|
|
28
|
+
* resolves a boolean, and a deferred entry IS unsupported, so the standard
|
|
29
|
+
* `isSupported().then(ok => ok && get…(app))` guard must run — not crash.
|
|
30
|
+
*/
|
|
31
|
+
export const isSupported = async (): Promise<boolean> => false;
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
// Type declarations. Aliased to the deferred placeholder so a consumer's own
|
|
35
|
+
// annotations keep type-checking: every deferred call returns `never`, which is
|
|
36
|
+
// assignable to any of these. Names that Firebase exports as a CLASS appear both
|
|
37
|
+
// here and above — a class is a value and a type, and both meanings must survive
|
|
38
|
+
// the swap.
|
|
39
|
+
export type Analytics = DeferredApi;
|
|
40
|
+
export type AnalyticsCallOptions = DeferredApi;
|
|
41
|
+
export type AnalyticsSettings = DeferredApi;
|
|
42
|
+
export type ConsentSettings = DeferredApi;
|
|
43
|
+
export type ConsentStatusString = DeferredApi;
|
|
44
|
+
export type ControlParams = DeferredApi;
|
|
45
|
+
export type Currency = DeferredApi;
|
|
46
|
+
export type CustomEventName = DeferredApi;
|
|
47
|
+
export type CustomParams = DeferredApi;
|
|
48
|
+
export type EventNameString = DeferredApi;
|
|
49
|
+
export type EventParams = DeferredApi;
|
|
50
|
+
export type GtagConfigParams = DeferredApi;
|
|
51
|
+
export type Item = DeferredApi;
|
|
52
|
+
export type Promotion = DeferredApi;
|
|
53
|
+
export type SettingsOptions = DeferredApi;
|
package/src/app/dispatch.test.ts
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
signInWithEmailAndPassword,
|
|
27
27
|
signOut,
|
|
28
28
|
} from '../auth/index.js';
|
|
29
|
-
import { get, getDatabase, ref, set } from '../database/index.js';
|
|
29
|
+
import { get, getDatabase, ref, sandbox as databaseSandbox, set } from '../database/index.js';
|
|
30
30
|
import { getStorage } from '../storage/index.js';
|
|
31
31
|
import { getAI } from '../ai/index.js';
|
|
32
32
|
import { getMessaging } from '../messaging/index.js';
|
|
@@ -121,6 +121,8 @@ describe('pyric/app — Firebase-shaped service containers', () => {
|
|
|
121
121
|
|
|
122
122
|
const dbA = getDatabase(a);
|
|
123
123
|
const dbB = getDatabase(b);
|
|
124
|
+
databaseSandbox.setDefaultPolicy(dbA, 'allow');
|
|
125
|
+
databaseSandbox.setDefaultPolicy(dbB, 'allow');
|
|
124
126
|
await set(ref(dbA, 'shared/value'), { from: authA.currentUser!.uid });
|
|
125
127
|
expect((await get(ref(dbB, 'shared/value'))).val()).toEqual({ from: authA.currentUser!.uid });
|
|
126
128
|
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pyric/app-check` — a DEFERRED mirror of `firebase/app-check`.
|
|
3
|
+
*
|
|
4
|
+
* App Check attests that a request comes from your app, via reCAPTCHA or a
|
|
5
|
+
* custom attestation provider talking to Google infrastructure. The sandbox
|
|
6
|
+
* has no attestation authority to model against, so the whole surface is
|
|
7
|
+
* deferred.
|
|
8
|
+
*
|
|
9
|
+
* Every symbol below resolves and links so an app that swaps `firebase` for
|
|
10
|
+
* `pyric` still loads; touching one throws `PyricDeferredApiError` naming this
|
|
11
|
+
* subpath. See `../deferred/entry.ts` for the full rationale.
|
|
12
|
+
*
|
|
13
|
+
* The value list is the exact public runtime surface of `firebase/app-check`
|
|
14
|
+
* (Firebase Web SDK 12.13.0). Keep it in sync when this entry graduates to a
|
|
15
|
+
* real mirror.
|
|
16
|
+
*/
|
|
17
|
+
import { deferredEntry, type DeferredApi } from '../deferred/entry.js';
|
|
18
|
+
|
|
19
|
+
export { PyricDeferredApiError } from '../deferred/entry.js';
|
|
20
|
+
|
|
21
|
+
export const {
|
|
22
|
+
CustomProvider, ReCaptchaEnterpriseProvider, ReCaptchaV3Provider, getLimitedUseToken,
|
|
23
|
+
getToken, initializeAppCheck, onTokenChanged, setTokenAutoRefreshEnabled,
|
|
24
|
+
} = deferredEntry('app-check');
|
|
25
|
+
|
|
26
|
+
// Type declarations. Aliased to the deferred placeholder so a consumer's own
|
|
27
|
+
// annotations keep type-checking: every deferred call returns `never`, which is
|
|
28
|
+
// assignable to any of these. Names that Firebase exports as a CLASS appear both
|
|
29
|
+
// here and above — a class is a value and a type, and both meanings must survive
|
|
30
|
+
// the swap.
|
|
31
|
+
export type AppCheck = DeferredApi;
|
|
32
|
+
export type AppCheckOptions = DeferredApi;
|
|
33
|
+
export type AppCheckToken = DeferredApi;
|
|
34
|
+
export type AppCheckTokenListener = DeferredApi;
|
|
35
|
+
export type AppCheckTokenResult = DeferredApi;
|
|
36
|
+
export type CustomProvider = DeferredApi;
|
|
37
|
+
export type CustomProviderOptions = DeferredApi;
|
|
38
|
+
export type PartialObserver = DeferredApi;
|
|
39
|
+
export type ReCaptchaEnterpriseProvider = DeferredApi;
|
|
40
|
+
export type ReCaptchaV3Provider = DeferredApi;
|
|
41
|
+
export type Unsubscribe = DeferredApi;
|
package/src/auth/instances.ts
CHANGED
|
@@ -80,7 +80,7 @@ export function getAuth(target?: Sandbox | FirebaseApp): Auth {
|
|
|
80
80
|
return handle;
|
|
81
81
|
}
|
|
82
82
|
throw new TypeError(
|
|
83
|
-
'pyric/auth is a sandbox-only mirror. Package resolution must leave firebase/auth unchanged for production; activate pyric
|
|
83
|
+
'pyric/auth is a sandbox-only mirror. Package resolution must leave firebase/auth unchanged for production; activate pyric sandbox or @pyric/cli/register before importing to select the sandbox.',
|
|
84
84
|
);
|
|
85
85
|
}
|
|
86
86
|
|