Pyric
Navigate

pyric/ai compatibility matrix

67.1% of the public API supported

147 of 219 public API

Status legend

Conforming: sandbox matches prod, locked by a passing probe Diverged (documented): intentional difference with a written reason

getAI(target) and dispatch

getAI(target)getAI(sandbox) returns an AI handle bound to the sandbox target; a model minted from it answers through the in-process answer engine
unit:instances.test.ts test ai#getai-sandbox-dispatch (no capture; structural dispatch claim)
getAI(target)After package resolution selects the mirror, getAI(app) uses the app's sandbox and the returned handle carries the app
unit:instances.test.ts test ai#getai-app-dispatch (package-resolution dispatch claim)
getAI(target)With no options the backend defaults to GoogleAIBackend, backendType is GOOGLE_AI, and the AI handle location is the empty string
unit:instances.test.ts test ai#getai-default-backend (matches upstream AIOptions default)
getAI(target)Repeat getAI calls with the same target return a stable handle
unit:instances.test.ts test ai#getai-idempotent (no capture; structural claim)
getAI(target, options)getAI(sandbox, { backend: new GoogleAIBackend(), engine: { kind: "scripted" } }) selects the scripted engine explicitly and behaves identically to the zero-config default
unit:instances.test.ts test ai#getai-engine-option (engine seam per packages/conformance/docs/ai/cdd-deltas.md)
VertexAIBackendVertexAIBackend carries backendType VERTEX_AI; its location and the resulting AI handle location default to us-central1
unit:instances.test.ts test ai#backend-vertex (matches upstream constructor default)
getGenerativeModel(ai, modelParams)A short model name such as gemini-flash-lite-latest normalizes to the models/ resource name on GenerativeModel.model
unit:instances.test.ts test ai#model-name-short (upstream AIModel normalization on the GoogleAI backend)
getGenerativeModel(ai, modelParams)A models/-prefixed name is accepted without double prefixing
unit:instances.test.ts test ai#model-name-prefixed (no capture; normalization claim)
normalization
getGenerativeModel(ai, modelParams)getGenerativeModel without modelParams.model throws an AIError with code no-model
unit:instances.test.ts test ai#model-name-required (upstream throw contract)
getAI(sandbox)The sandbox target with the scripted engine performs no network I/O for generateContent
unit:instances.test.ts test ai#getai-sandbox-no-network (ruling 1 of the engine placement deltas: the scripted engine does no I/O anywhere)

GenerativeModel.generateContent envelope

generateContent()The response envelope top-level key set is exactly candidates, modelVersion, responseId, usageMetadata
Capture ai-generate-minimal-envelope replayed by packages/pyric/test/ai/generate-content.test.ts test ai#generate-envelope-keys (Structured evidence: ai-generate-minimal-envelope, generate-content.test.ts)
generateContent()The candidate key set is content, finishReason, index, and index is present on the wire (0 for the single candidate)
Capture ai-generate-minimal-envelope (candidateHasIndexOnWire) replayed by packages/pyric/test/ai/generate-content.test.ts test ai#generate-candidate-keys (Structured evidence: ai-generate-minimal-envelope, generate-content.test.ts)
generateContent()Candidate content carries role model and the content key set is parts, role
Capture ai-generate-minimal-envelope replayed by packages/pyric/test/ai/generate-content.test.ts test ai#generate-role-model (Structured evidence: ai-generate-minimal-envelope, generate-content.test.ts)
generateContent()A normal completion finishes with finishReason STOP
Capture ai-generate-minimal-envelope replayed by packages/pyric/test/ai/generate-content.test.ts test ai#generate-finish-stop (Structured evidence: ai-generate-minimal-envelope, generate-content.test.ts)
generateContent()The usageMetadata key set on a minimal text call is candidatesTokenCount, promptTokenCount, promptTokensDetails, serviceTier, totalTokenCount
Capture ai-generate-minimal-envelope replayed by packages/pyric/test/ai/generate-content.test.ts test ai#generate-usage-key-set (Structured evidence: ai-generate-minimal-envelope, generate-content.test.ts)
generateContent()usageMetadata.serviceTier rides the wire even though the 2.12.0 SDK typings do not declare it
Capture ai-generate-minimal-envelope (usageServiceTierPresent) replayed by packages/pyric/test/ai/generate-content.test.ts test ai#generate-usage-service-tier (Structured evidence: ai-generate-minimal-envelope, generate-content.test.ts)
generateContent()modelVersion and responseId are present nonempty strings; the sandbox mints them deterministically
Capture ai-generate-minimal-envelope replayed by packages/pyric/test/ai/generate-content.test.ts test ai#generate-modelversion-responseid (Structured evidence: ai-generate-minimal-envelope, generate-content.test.ts)
generateContent(request)A plain string request is wrapped as a single user turn before it reaches the engine
unit:generate-content.test.ts test ai#generate-string-request (no capture; upstream request formatting claim)
generateContent(request)A top-level systemInstruction is accepted and the response envelope shape is unaffected
Capture ai-system-instruction-accepted replayed by packages/pyric/test/ai/generate-content.test.ts test ai#generate-system-instruction (Structured evidence: ai-system-instruction-accepted, generate-content.test.ts)
generateContent(request)responseMimeType application/json plus a responseSchema yields a text part that parses as JSON with the schema key set
Capture ai-structured-output-shape replayed by packages/pyric/test/ai/generate-content.test.ts test ai#generate-structured-output (Structured evidence: ai-structured-output-shape, generate-content.test.ts)
generateContent(request)With thinkingConfig on the probe model, text parts carry thoughtSignature and no part is flagged thought: true
Capture ai-thinking-thought-parts (partKeySets, anyThoughtPart false) replayed by packages/pyric/test/ai/generate-content.test.ts test ai#generate-thinking-signature (Structured evidence: ai-thinking-thought-parts, generate-content.test.ts)
generateContent(request, singleRequestOptions)A pre-aborted SingleRequestOptions.signal rejects the call
unit:generate-content.test.ts test ai#generate-abort-signal (no capture; upstream SingleRequestOptions contract)
generateContent()Token counts are minted without a tokenizer, and the minimal envelope omits safetyRatings, matching the captured candidate key set
Capture ai-generate-minimal-envelope replayed by packages/pyric/test/ai/generate-content.test.ts test ai#generate-decoration-synthesized (Structured evidence: ai-generate-minimal-envelope, generate-content.test.ts)

generateContentStream framing and aggregation

generateContentStream()result.stream async-iterates response chunks via for await; each chunk is a complete GenerateContentResponse
Capture ai-generate-stream-framing replayed by packages/pyric/test/ai/streaming.test.ts test ai#stream-async-iterable (Structured evidence: ai-generate-stream-framing, streaming.test.ts)
generateContentStream() wire framingEvery SSE event is data: prefixed and its payload parses as a complete JSON document
Capture ai-generate-stream-framing (allEventsDataPrefixed) replayed byte-level by packages/pyric/test/ai/streaming.test.ts test ai#stream-data-prefixed (Structured evidence: ai-generate-stream-framing, streaming.test.ts)
generateContentStream() wire framingSSE events are separated by CRLF CRLF
Capture ai-generate-stream-framing (separatorIsCrlfCrlf) replayed byte-level by packages/pyric/test/ai/streaming.test.ts test ai#stream-separator-crlf (Structured evidence: ai-generate-stream-framing, streaming.test.ts)
generateContentStream()finishReason appears only on the last chunk of a stream
Capture ai-generate-stream-framing (finishReasonOnlyOnLastChunk) replayed by packages/pyric/test/ai/streaming.test.ts test ai#stream-finish-last-chunk (Structured evidence: ai-generate-stream-framing, streaming.test.ts)
generateContentStream()usageMetadata rides every chunk, not only the last one
Capture ai-generate-stream-framing (usageMetadataChunkIndexes covers all chunks) replayed by packages/pyric/test/ai/streaming.test.ts test ai#stream-usage-every-chunk (Structured evidence: ai-generate-stream-framing, streaming.test.ts)
generateContentStream()Every chunk carries candidates or usageMetadata
Capture ai-generate-stream-framing (everyEventHasCandidatesOrUsage) replayed by packages/pyric/test/ai/streaming.test.ts test ai#stream-chunk-envelope (Structured evidence: ai-generate-stream-framing, streaming.test.ts)
generateContentStream()result.response resolves to an aggregated response whose text is the concatenation of the streamed text parts
unit:streaming.test.ts test ai#stream-response-aggregate (aggregation semantics; text values come from an explicit script)
generateContentStream()The aggregated response carries the final chunk finishReason and usageMetadata
unit:streaming.test.ts test ai#stream-aggregate-final-meta (aggregation semantics derived from the framing capture)
metadata carry

ChatSession history and streaming turns

GenerativeModel.startChat()startChat returns a ChatSession seeded with StartChatParams.history
unit:upstream-ai-probes.test.ts (I1 validateChatHistory accept/reject via startChat) + unit:chat-session.test.ts test ai#chat-startchat
ChatSession.sendMessage() / getHistory()sendMessage appends the user turn and the model turn; getHistory() returns the ordered Content[] with alternating roles
unit:chat-session.test.ts test ai#chat-history-threads (no capture; history threading claim)
clone
ChatSession.getHistory()Blocked prompts and blocked candidates are excluded from getHistory()
unit:chat-session.test.ts test ai#chat-history-excludes-blocked (upstream JSDoc contract; exercised with a scripted blocked envelope)
blocked history
ChatSession.sendMessage()A sendMessage result carries the same envelope facts as generateContent: the four top-level keys and role model
Capture ai-generate-minimal-envelope replayed by packages/pyric/test/ai/chat-session.test.ts test ai#chat-sendmessage-envelope (Structured evidence: ai-generate-minimal-envelope, chat-session.test.ts)
ChatSession.sendMessageStream()sendMessageStream returns a stream plus a response promise; history updates after aggregation completes
unit:chat-session.test.ts test ai#chat-sendmessagestream (no capture; streaming turn claim)
ChatSession.sendMessageStream()Exactly one user turn is recorded per sendMessageStream call; the mirror implements the 2.13.0 fixed semantics, not the installed 2.12.0 duplicate-user-turn bug
unit:chat-session.test.ts test ai#chat-stream-single-user-turn (no capture; divergence pinned by ruling, see notes)
2.13.0 semantics
POSSIBLE_ROLESPOSSIBLE_ROLES is exactly ["user", "model", "function", "system"]
unit:chat-session.test.ts test ai#chat-role-vocabulary (upstream constant; distinct from the production wire role vocabulary in ai-error-bad-role)

Function calling

functionCall partsA functionCall part carries the key set args, id, name, and args arrives as a parsed JSON object, not a string
Capture ai-function-call-shape (functionCallKeySet, argsIsObjectNotString) replayed by packages/pyric/test/ai/function-calling.test.ts test ai#fncall-part-shape (Structured evidence: ai-function-call-shape, function-calling.test.ts)
toolConfig.functionCallingConfigMode ANY forces a functionCall part in the response and the candidate finishes STOP
Capture ai-function-call-shape (captured under mode ANY, finishReason STOP) replayed by packages/pyric/test/ai/function-calling.test.ts test ai#fncall-mode-any (Structured evidence: ai-function-call-shape, function-calling.test.ts)
functionCall partsfunctionCall.id is present on the GoogleAI wire; the mirror emits an id on synthesized calls
Capture ai-function-call-shape (id in functionCallKeySet) replayed by packages/pyric/test/ai/function-calling.test.ts test ai#fncall-id-present (Structured evidence: ai-function-call-shape, function-calling.test.ts)
functionResponse round tripA round trip that threads the model functionCall turn back verbatim, thoughtSignature preserved, is accepted: the answer has a text part and no further functionCall part
Capture ai-function-response-round replayed by packages/pyric/test/ai/function-calling.test.ts test ai#fncall-round-trip (Structured evidence: ai-function-response-round, function-calling.test.ts)
functionResponse round tripA replayed model functionCall turn lacking thoughtSignature is rejected 400 INVALID_ARGUMENT with the thought-signature message
Capture ai-error-fncall-missing-thought-signature replayed by packages/pyric/test/ai/function-calling.test.ts test ai#fncall-thought-signature-required (Structured evidence: ai-error-fncall-missing-thought-signature, function-calling.test.ts)
scripted engine synthesisThe engine mints a thoughtSignature on every functionCall part it synthesizes, so scripted tool round trips replay cleanly
unit:function-calling.test.ts test ai#fncall-signature-minted (capture ai-error-fncall-missing-thought-signature cited as the motivating rejection) (Structured evidence: ai-error-fncall-missing-thought-signature)

countTokens

countTokens()The countTokens envelope key set is exactly promptTokensDetails, totalTokens
Capture ai-counttokens-envelope replayed by packages/pyric/test/ai/errors-counttokens.test.ts test ai#counttokens-envelope (Structured evidence: ai-counttokens-envelope, errors-counttokens.test.ts)
countTokens()An identical payload returns an identical totalTokens across calls
Capture ai-counttokens-envelope (deterministicAcrossTwoCalls) replayed by packages/pyric/test/ai/errors-counttokens.test.ts test ai#counttokens-deterministic (Structured evidence: ai-counttokens-envelope, errors-counttokens.test.ts)

Error envelopes

error envelopeA model name production has never served fails 404 NOT_FOUND with the error key set code, message, status and no details
Capture ai-error-unknown-model replayed by packages/pyric/test/ai/errors-counttokens.test.ts test ai#error-unknown-model (Structured evidence: ai-error-unknown-model, errors-counttokens.test.ts)
error envelopeA retired model family (Gemini 1.5) fails 404 NOT_FOUND with an ErrorInfo detail and a retirement message distinct from unknown-model
Capture ai-error-retired-model replayed by packages/pyric/test/ai/errors-counttokens.test.ts test ai#error-retired-model (Structured evidence: ai-error-retired-model, errors-counttokens.test.ts)
error envelopeAn invalid API key fails 400 INVALID_ARGUMENT, not 401, with ErrorInfo plus LocalizedMessage details and the message API key not valid. Please pass a valid API key.
Capture ai-error-bad-api-key replayed by packages/pyric/test/ai/errors-counttokens.test.ts test ai#error-bad-api-key (Structured evidence: ai-error-bad-api-key, errors-counttokens.test.ts)
error envelopeAn empty contents array fails 400 INVALID_ARGUMENT with the message contents is not specified
Capture ai-error-empty-contents replayed by packages/pyric/test/ai/errors-counttokens.test.ts test ai#error-empty-contents (Structured evidence: ai-error-empty-contents, errors-counttokens.test.ts)
error envelopeAn invalid content role fails 400 INVALID_ARGUMENT and the message lists the production role vocabulary: SYSTEM, SYSTEM_1, USER, ASSISTANT, DEVELOPER, CONTEXT, USER_CONTEXT, MODEL, USER
Capture ai-error-bad-role replayed by packages/pyric/test/ai/errors-counttokens.test.ts test ai#error-bad-role (Structured evidence: ai-error-bad-role, errors-counttokens.test.ts)
AIErrorHTTP failures surface as AIError with an AIErrorCode code and customErrorData carrying status, statusText, and errorDetails
unit:errors-counttokens.test.ts test ai#error-aierror-shape (capture ai-error-bad-api-key cited as the sample envelope) (Structured evidence: ai-error-bad-api-key)
AIErrorCodeAIErrorCode exposes the 14 documented codes, from error through unsupported
unit:errors-counttokens.test.ts test ai#error-code-vocabulary (upstream constant vocabulary)

Response helpers (EnhancedGenerateContentResponse)

response.text()text() concatenates the text parts of the first candidate
unit:upstream-ai-probes.test.ts (I3 text() across mixed parts) + unit:helpers-schema.test.ts test ai#helper-text
response.text()text() throws on bad finish reasons such as SAFETY and on a blocked prompt
unit:helpers-schema.test.ts test ai#helper-text-throws (exercised with a scripted SAFETY envelope)
response.functionCalls()functionCalls() returns the FunctionCall array from the functionCall parts, args as parsed objects
unit:upstream-ai-probes.test.ts (I3 text+functionCall mix) + Capture ai-function-call-shape replayed by unit:helpers-schema.test.ts test ai#helper-functioncalls (Structured evidence: ai-function-call-shape)
response.thoughtSummary()thoughtSummary() returns undefined when no part is flagged thought: true, the captured lite-model case
unit:upstream-ai-probes.test.ts (I3 thoughtSummary from thought parts) + Capture ai-thinking-thought-parts replayed by unit:helpers-schema.test.ts test ai#helper-thoughtsummary (Structured evidence: ai-thinking-thought-parts)
response.inlineDataParts()inlineDataParts() returns the InlineDataPart array when inlineData parts exist and undefined when none do
unit:helpers-schema.test.ts test ai#helper-inlinedataparts (exercised with a scripted raw envelope)
response helpersHelpers tolerate omitted decoration: an envelope without usageMetadata, finishReason, or safetyRatings still serves text() without throwing
unit:helpers-schema.test.ts test ai#helper-tolerates-missing-decor (exercised with a scripted bare envelope)

Schema builders

Schema.object()Schema.object serializes to type object with properties, and required is derived by excluding optionalProperties
unit:upstream-ai-probes.test.ts (I2 empty optionalProperties + propertyOrdering) + unit:helpers-schema.test.ts test ai#schema-object-tojson
Schema.enumString()Schema.enumString serializes the enum values with type string and format enum
unit:helpers-schema.test.ts test ai#schema-string-enum (upstream toJSON request shape; GoogleAI accepts only enum and date-time formats)
format
Schema.string()/integer()/number()/boolean()/array()Each primitive builder serializes its SchemaType, and array carries items
unit:helpers-schema.test.ts test ai#schema-primitives (upstream toJSON request shape)
Schema.anyOf()Schema.anyOf returns an AnyOfSchema whose JSON carries an anyOf array of sub-schemas and no top-level type
unit:upstream-ai-probes.test.ts (I2 empty anyOf → invalid-schema) + unit:helpers-schema.test.ts test ai#schema-anyof
generationConfig.responseSchemaA built Schema serializes into generationConfig.responseSchema on the request and drives JSON output
Capture ai-structured-output-shape replayed by packages/pyric/test/ai/helpers-schema.test.ts test ai#schema-rides-request (Structured evidence: ai-structured-output-shape, helpers-schema.test.ts)

Sandbox answer engine: scripted

scripted engineWith no script the engine returns a deterministic synthesized response derived from the request, wire-true in shape: the captured envelope key sets hold
unit:engines.test.ts test ai#scripted-zero-config (capture ai-generate-minimal-envelope cited as the shape source) (Structured evidence: ai-generate-minimal-envelope)
scripted engineThe same unscripted request twice yields an identical envelope, candidates and usage included
unit:engines.test.ts test ai#scripted-deterministic (determinism claim from the scripted authoring deltas)
script(ai, entries)Script entries without matchers are consumed in FIFO queue order
unit:engines.test.ts test ai#scripted-queue-order (ruling 2 of the scripted authoring deltas)
script(ai, entries)Entries match by substring, regex, or predicate on the request; a matching entry wins over the plain queue
unit:engines.test.ts test ai#scripted-matchers (ruling 2 of the scripted authoring deltas)
script(ai, entries)A raw Gemini envelope entry is returned verbatim, so an observation behavior.raw pastes in directly and captures are the corpus
unit:engines.test.ts test ai#scripted-raw-envelope (ruling 3 of the scripted authoring deltas)
script(ai, entries)A text shorthand expands to a wire-true envelope: finishReason STOP, usageMetadata with serviceTier, modelVersion, responseId
unit:engines.test.ts test ai#scripted-shorthand-text (capture ai-generate-minimal-envelope cited as the expansion target) (Structured evidence: ai-generate-minimal-envelope)
script(ai, entries)A functionCall shorthand expands to a model turn whose functionCall part carries a minted thoughtSignature
unit:engines.test.ts test ai#scripted-shorthand-functioncall (capture ai-error-fncall-missing-thought-signature cited as the motivating rejection) (Structured evidence: ai-error-fncall-missing-thought-signature)
script(ai, entries)A chunk-array shorthand declares chunk boundaries and the engine applies the captured framing, so authors never hand-write SSE
unit:engines.test.ts test ai#scripted-stream-chunks (capture ai-generate-stream-framing cited as the framing source) (Structured evidence: ai-generate-stream-framing)
script(ai, entries)Scripted text is the one place generated text values may be asserted: response.text() returns the scripted string exactly
unit:engines.test.ts test ai#scripted-text-assertable (evidence tier ruling 1: generated text is never compared anywhere else)

Sandbox answer engine: openai translation

openai engineGemini contents and systemInstruction translate to OpenAI chat messages, and the OpenAI response translates back to a Gemini envelope with role model
unit:engines.test.ts test ai#openai-request-translation (translation exercised against a local OpenAI-compatible mock)
openai engineOpenAI tool_call ids are matched FIFO against Gemini functionResponse parts when replaying tool history
unit:engines.test.ts test ai#openai-fifo-tool-ids (lossy translation edge from ticket #96)
openai engineStreamed OpenAI tool_call deltas are buffered; the Gemini stream emits whole functionCall parts with parsed args, never partial fragments
unit:engines.test.ts test ai#openai-buffered-fncalls (lossy translation edge from ticket #96)
openai engineThe OpenAI [DONE] sentinel is never forwarded as a Gemini chunk; every emitted chunk is a parseable Gemini envelope
unit:engines.test.ts test ai#openai-done-not-forwarded (lossy translation edge from ticket #96)
openai engineParts flagged thought: true in history are skipped when replaying to an OpenAI upstream
unit:engines.test.ts test ai#openai-thought-parts-skipped (lossy translation edge from ticket #96)

Current gaps

Documented divergences

Known differences between Pyric and production Firebase. Each remains tracked as a non-conforming row.

getGenerativeModel(ai, modelParams)A models/-prefixed name is accepted without double prefixing
unit:instances.test.ts test ai#model-name-prefixed (no capture; normalization claim)
generateContentStream()The aggregated response carries the final chunk finishReason and usageMetadata
unit:streaming.test.ts test ai#stream-aggregate-final-meta (aggregation semantics derived from the framing capture)
ChatSession.sendMessage() / getHistory()sendMessage appends the user turn and the model turn; getHistory() returns the ordered Content[] with alternating roles
unit:chat-session.test.ts test ai#chat-history-threads (no capture; history threading claim)
ChatSession.getHistory()Blocked prompts and blocked candidates are excluded from getHistory()
unit:chat-session.test.ts test ai#chat-history-excludes-blocked (upstream JSDoc contract; exercised with a scripted blocked envelope)
ChatSession.sendMessageStream()Exactly one user turn is recorded per sendMessageStream call; the mirror implements the 2.13.0 fixed semantics, not the installed 2.12.0 duplicate-user-turn bug
unit:chat-session.test.ts test ai#chat-stream-single-user-turn (no capture; divergence pinned by ruling, see notes)
Schema.enumString()Schema.enumString serializes the enum values with type string and format enum
unit:helpers-schema.test.ts test ai#schema-string-enum (upstream toJSON request shape; GoogleAI accepts only enum and date-time formats)

Reviewed public-runtime gaps

ai.imagen-retirementImagen is deprecated upstream; all Imagen models shut down as early as June 2026 (upstream 2.11.0 deprecation). Mirroring an API whose production counterpart is retiring would freeze dead behavior.
ImagenAspectRatio ImagenImageFormat ImagenModel ImagenPersonFilterLevel ImagenSafetyFilterLevel getImagenModel
upstream:firebase/ai@2.11.0
ai.hybrid-inferenceHybrid/on-device inference is deferred, not out of scope — the sandbox runs in the browser and can model the on-device path through the answer-engine seam; the mode has not been wired yet.
InferenceMode
upstream:firebase/ai
ai.inference-sourceDeferred with hybrid/on-device inference — the source marker is meaningful once the hybrid mode lands.
InferenceSource
disposition:ai.hybrid-inference
ai.live-apiLive API is deferred, not out of scope — it is a bidirectional websocket protocol the sandbox can model with a scripted session engine, the same seam pattern the REST plane already uses; the work has not happened yet.
LiveGenerativeModel LiveResponseType LiveSession getLiveGenerativeModel
upstream:firebase/ai
ai.server-templatesServer-side templates are deferred, not out of scope — hosting template state is exactly the kind of server infrastructure the sandbox exists to model; the template store has not been built yet.
TemplateGenerativeModel getTemplateGenerativeModel
upstream:firebase/ai
ai.template-imagen-retirementTemplate-served Imagen models retire with the rest of Imagen (deprecated upstream, June 2026 shutdown); the template transport does not outlive the model family it serves.
TemplateImagenModel getTemplateImagenModel
upstream:firebase/ai@2.11.0
ai.live-audioDeferred with the rest of the Live API family — the browser audio helper rides the Live session seam once that lands.
startAudioConversation
disposition:ai.live-api