v0.6.1
Added
GET /healthnow also reports the runningversion, read frompackage.json, so a deployed container can be matched against the release and image tag it was built from.- Gemini (AI Studio) provider mounted at
/gemini, served on bothv1betaandv1: model catalog (GET /models,GET /models/{model}, emptyGET /tunedModels), validated against the official@google/genaiSDK. POST /gemini/v1beta/models/{model}:generateContent, withcandidates,finishReason,usageMetadatasplit by modality,modelVersionandresponseId, honoringcandidateCountand countingsystemInstructiontowards the prompt tokens.POST /gemini/v1beta/models/{model}:streamGenerateContent: SSE with?alt=sseand a streamed JSON array without it. No[DONE]sentinel, which the Gemini SDK would reject as an incomplete JSON segment.- Gemini Enterprise provider (the platform formerly called Vertex AI) mounted at
/gemini-enterprise, on bothv1beta1andv1: publisher model catalog underpublisherModels, served identically on the regionalprojects/{p}/locations/{l}/publishers/google/models/…path and the expresspublishers/google/models/…one. - Azure OpenAI provider mounted at
/azure/openai, serving both of Azure's surfaces: the classic deployment-based one (/openai/deployments/{deployment}/…?api-version=, with the version required on every call) and the newer/openai/v1one, which is OpenAI's contract verbatim. Model catalog on both. - Anthropic Files (beta): multipart upload, metadata, listing filterable by
scope_id, download and delete, with the file's metadata carried inside the id it is minted so an upload round-trips without a store. Every call is gated onanthropic-beta: files-api-2025-04-14— comma-separated or repeated, both accepted — while the?beta=truethe SDK appends is optional. A file you uploaded answers403on download, as on the real API; the simulated catalog carries two API-produced files so the download path is testable. - A message whose only content is a file attachment now echoes the attachment (
Echo: [file_…]) instead of falling through to the generic greeting, so a test can assert the reference arrived. MultipartFileinsrc/core/multipart.tsnow also carries the part's ownContent-Type, which Anthropic reports asmime_type(bare, with the charset the SDK'stoFileappends stripped).- Anthropic Message Batches, kept stateless by encoding the whole batch into its own
msgbatch_id: create, retrieve, results as JSONL keyed bycustom_id, cancel and delete, plus a list endpoint that is always empty. A batch isendedon arrival, so anx-llm-mock-batch-statusheader pinsin_progressorcancelingwhen a test needs the polling branch. Encoding bounds the size: past ~1600 characters of id, creation fails loudly instead of dropping requests. POST /anthropic/v1/messages/count_tokens, summing messages,systemand the serialized tool declarations the same way/v1/messagesbuildsusage.input_tokens, so counting then sending reports the same number twice.- Anthropic streaming with named SSE events —
message_start→content_block_start/_delta/_stop→message_delta→message_stop, withinput_json_deltafor tool arguments and a separatesignature_deltaafter a thinking block. Both theevent:lines and themessage_stopterminator are required by the official client, making this the third SSE convention among the five providers. POST /anthropic/v1/messages: content blocks, a top-levelsystemcounted towardsinput_tokens,stop_reason/stop_sequence, tool use astool_useblocks with decodedinputandtoolu_ids,tool_choicein its object form, andthinkingblocks whose textdisplaydecides.max_tokensis required, as on the real API, and the conversation must open with the user.src/core/tools.tsnow also reads a tool declared withinput_schema, the fourth shape after OpenAI's nestedfunction, the Responses API's top-levelparameters, and Gemini'sfunctionDeclarations.- Anthropic provider mounted at
/anthropic, validated against the official@anthropic-ai/sdk: model catalog with cursor pagination (after_id/before_id) and the real capability tree, authentication throughx-api-key(withAuthorization: Bearerfor OAuth callers), a requiredanthropic-versionheader, and the doubly-wrapped error envelope whoserequest_idmatches thex-request-idresponse header. Managed Agents is deliberately out of scope. - Azure content filtering:
prompt_filter_resultson the response andcontent_filter_resultson every choice, plus anx-llm-mock-content-filterheader (<target>or<target>:<category>:<severity>) to pin the three documented failure paths — a blocked prompt as a400carrying its verdict ininnererror, a filtered completion asfinish_reason: "content_filter"with null content, and the "filter did not run" case. Streamed responses open with a choice-less chunk carrying the prompt's verdict, as Azure's do. finish_reasonon the shared OpenAI chat types now admits"content_filter", which is part of that contract even though the OpenAI provider never produces it.- Azure's
/openai/v1surface, serving the same inference handlers with noapi-versionand no deployments, so a plainOpenAIclient reaches it with only abaseURLchange. Both surfaces return the same body for the same request. chat/completions(with SSE streaming and tool calls) andembeddingson Azure's deployment path, reusing the OpenAI provider's services since the payloads are OpenAI's verbatim. The handlers are mounted so the/openai/v1surface can share them.- Azure authentication through the
api-keyheader, withAuthorization: Beareraccepted for Entra ID callers. A missing credential answers in the API gateway's flatter{statusCode, message}shape; an invalid one in the wrapped{error:{code, message}}envelope. - Any deployment name resolves, except those prefixed
missing-, which return the real404 DeploymentNotFound— the same reserved-name convention already used for files on OpenAI and Gemini. - Interactions API on Gemini Enterprise, reachable on both the express
/v1beta1/interactionspath and the regional one, where the SDK percent-encodes the whole version-project-location component into a single path segment that a literal version mount cannot match. The router itself now lives insrc/providers/google-shared/routes/interactions.ts, shared verbatim by both Google providers. - Gemini Enterprise embeddings through
:predict, the generic prediction endpoint this platform uses instead of AI Studio's:embedContent: aninstances/predictionsenvelope,outputDimensionalityset once per call in a siblingparametersobject, andstatistics.token_countin snake_case. Plus:countTokens, which reportstotalTokensalone. :generateContentand:streamGenerateContenton Gemini Enterprise, on both path shapes, reusing the shared generative core and adding thecreateTimethis platform stamps on every response. Tool calls,candidateCountand the mock headers all carry over.- Dual authentication for that provider, validated against the same key set: an OAuth bearer token (regional) failing as
401 UNAUTHENTICATED, and anx-goog-api-key(express) failing as400 INVALID_ARGUMENT.AuthScheme.invalidKeyErrornow receives the request so a provider can tell its transports apart. - Gemini's OpenAI-compatibility layer at
/gemini/v1beta/openai:chat/completions(streaming and tool calls included),embeddingsat Gemini's dimensions, andmodelsserving Gemini's catalog in OpenAI's list envelope. Only what Google actually exposes is mounted, so the Responses API and Files404there as they do against the real service. - Gemini embeddings:
:batchEmbedContents(what the SDK calls even for a single input) and the singular:embedContent, with hash-seeded unit vectors, per-model native dimensions (3072 forgemini-embedding-001, 768 for the older models) andoutputDimensionalitytruncation. :countTokens, accepting either a barecontentslist or a wholegenerateContentRequestso a system instruction and tool declarations are counted too.- Gemini Files API: the two-step resumable upload at
POST /gemini/upload/v1beta/files, plusGET /fileswithpageSize/pageToken,GET /files/{name}and an idempotentDELETE. - Stateless resumable uploads: the session metadata rides inside the
x-goog-upload-urlhanded to the client, and the file's metadata inside the name it is minted, soupload→getround-trips on any instance without a store.sha256Hashis the real digest of the bytes received. - Interactions API, Google's next-generation surface:
POST /gemini/v1beta/interactionswithsteps, snake_caseusageand tool calls,GET /interactions/{id}synthesized statelessly (with?stream=truereplay),POST /interactions/{id}/canceland an idempotentDELETE. - Interactions streaming:
interaction.created→step.start/step.delta/step.stop→interaction.completed, witharguments_deltapieces for a function call. - Gemini tool calls:
tools[].functionDeclarations[]andtoolConfig.functionCallingConfig(ANY,NONE,AUTO,allowedFunctionNames), emitted asfunctionCallparts whoseargsis a decoded object. Loops terminate on afunctionResponsepart, and both mock headers apply. - Gemini authentication via
x-goog-api-key, with the?key=query parameter andAuthorization: Beareralso accepted. The API key set is shared across providers. - Gemini error envelopes:
google.rpc.Status(code/message/status/details) on the classic surface, and the flatter Interactions-style envelope under/interactions. - Routing for Google's
{resource}:{method}custom methods, somodels/gemini-3.6-flash:generateContentreaches a handler instead of tripping over Express':parameter syntax. - Tool calling on both Chat Completions and the Responses API:
tool_choice: "required"or a named function returns real tool calls, with arguments synthesized from the tool's JSON Schema (default, then the firstenumvalue, then a per-type placeholder). x-llm-mock-tool-callsheader to pin the exact tool calls a response must contain, including parallel calls and verbatim (even malformed) argument strings.- Tool calls in streaming:
delta.tool_callschunks keyed byindexwithfinish_reason: "tool_calls"on Chat Completions, andresponse.function_call_arguments.delta/.doneevents on the Responses API. - Agent loops terminate: a
role: "tool"message or afunction_call_outputitem stopstool_choicefrom forcing further calls.
Changed
- The README is down from 778 lines to 175: it now covers what the project is, why you would use it, how to start it and how to control its replies, and points at llm-mock.dev for the rest. The per-endpoint detail it used to carry — 433 lines of it — lives on the documentation site, one page per provider, which is where it stays current.
- The documentation site's API reference is now one page per provider —
api-openai.html,api-anthropic.html,api-gemini.html,api-gemini-enterprise.html,api-azure.html— withapi.htmlkeeping what they share (base URLs, authentication, response-control headers, provider support) and linking out to each. The stylesheet moved todocs/styles.cssrather than being copied inline into six pages;sitemap.xmllists all seven URLs. Every section id was preserved, so existing deep links still resolve. - The Responses API now echoes the
toolsandtool_choiceit received instead of always reporting[]and"auto". - Tests are now grouped by provider under
tests/openai/andtests/gemini/, sharing the server harness intests/server.ts. - Tool-calling logic moved from
src/providers/openai/services/tools.tstosrc/core/tools.ts, now that a second provider uses it. - Deterministic embedding vectors moved to
src/core/embeddings.ts, shared by both providers; what differs between them is the wire envelope and the default dimension count, not the vector. - Google's two Gemini surfaces now share a
src/providers/google-shared/module holding the Content/Part model,generateContent,countTokens, the Interactions API and the{resource}:{method}path parser. The Gemini provider keeps what only AI Studio has — its model catalog, the Files API and:embedContent— ahead of a Gemini Enterprise provider that shares the generative core but embeds through:predictand has no Files API at all.
Fixed
The Gemini provider was verified end to end against the live generativelanguage.googleapis.com, which corrected several shapes that had been built from documentation alone:
- Error
detailsare emitted only for an invalid API key, and withdomain: "googleapis.com"plus agoogle.rpc.LocalizedMessageentry. Every other error — missing credential, unknown model, unknown file, malformed body — carries nodetailsat all. - A credential failure under
/interactionsnow answers in the classicgoogle.rpc.Statusenvelope rather than that surface's next-gen one, because Google's frontend rejects the key before the service runs. The service's own errors keep the next-gen envelope. - The OpenAI-compatibility layer answers credential failures with its own codes:
404 NOT_FOUNDfor a missing credential (not403) and a shorterPlease pass a valid API keywith no details for a bad one. - The compatibility layer now returns Google's error envelope rather than OpenAI's, including the
GenerateContentRequest.contentscomplaint for a request with nomessages, and its model objects carrydisplay_namewith nocreated. Completions no longer report asystem_fingerprintor achatcmpl-id prefix. - Interaction objects gained
object: "interaction",service_tier, and thetotal_cached_tokens/total_tool_use_tokens/total_thought_tokenscounters; their modality is lowercasetext, they carry nooutput_tokens_by_modality, their timestamps have no fractional seconds, and a create no longer emits auser_inputstep. - Catalog ids
gemini-3.1-proandgemini-3-flashdo not exist; they aregemini-3.1-pro-previewandgemini-3-flash-preview. - The Files
403echoes the id as the caller wrote it, without thefiles/prefix. - Tool argument synthesis no longer yields
nullfor every property of a Gemini schema: types are matched case-insensitively, so Gemini's uppercaseTypeenum (STRING,OBJECT, ...) resolves like lowercase JSON Schema types.