@kindgi/adapter-model-gemini
npm install @kindgi/adapter-model-gemini · source
Gemini ModelProvider for @kindgi/capabilities, on Google Cloud's Vertex AI. Wraps the official @google/genai client behind the framework's provider-neutral interface. One provider exposes every model in its metadata.models[]; ModelCallInput.model picks the model per call.
Credentials
Section titled “Credentials”No API key. The client uses Google Application Default Credentials:
- on a laptop,
gcloud auth application-default login; - on Cloud Run and other Google Cloud runtimes, the attached service account;
- elsewhere, a service-account key (its JSON) passed as
credentials— through a provider row'ssecret_refwhen registered with a Kindgi server.
A Kindgi server with KINDGI_TENANT_HOST_ACCESS=isolated doesn't let tenant configuration sign in as the server (serverCredentials: false): a registration there needs secret_ref, and without one the check and the factory refuse it, naming secret_ref.
The account needs the Vertex AI User role (roles/aiplatform.user) in the project.
Translation
Section titled “Translation”One non-streaming models.generateContent call per invoke:
systemmessages lift intosystemInstruction(several join with a blank line).usermessages becomeuserturns,assistantmessagesmodelturns, and consecutive turns of one role merge.- Function calling. Tool definitions become
functionDeclarations, with the tool's JSON Schema asparametersJsonSchema. Assistant tool calls replay asfunctionCallparts. Tool results becomefunctionResponseparts, named after the call they answer, with the tool's output underresponse.output(parsed when it's JSON).- Tool names go over the wire as they are. Gemini accepts
<pack>.<tool>ids: a letter or_first, then letters, digits and_ . : -, at most 64 characters. Other names are refused.
- Tool names go over the wire as they are. Gemini accepts
- Thought signatures. Each
functionCallpart'sthoughtSignaturecomes back asModelToolCall.signature, and goes out again on the same call when the conversation continues. Gemini's thinking models need it to keep their reasoning across a tool round trip. - Thinking parts (
thought: true) are left out of the message text. Thinking tokens count as completion tokens, since they bill as output. - Structured output.
structuredOutputbecomesresponseMimeType: 'application/json'plusresponseJsonSchema. temperature,maxOutputTokens(the call's, else the model'sModelInfo.maxOutputTokens) andabortSignalare passed through.- Retries. A call that fails with a transient status (408, 429, 500, 502, 503, 504) is retried: three attempts in all, backing off from a second, with jitter. The
@google/genaiclient retries only when it's told to, and the adapter tells it. A failed connection isn't retried. The result'sattemptscounts every attempt; a call that still fails throws the last error, andattemptsOf(error)(@kindgi/capabilities/attempts) counts them. An injectedclientkeeps its own settings. finishReason:- a response with function calls →
tool-use; STOPor unspecified →stop;MAX_TOKENS→length;- the safety, recitation, blocklist and sensitive-data stops, or a blocked prompt →
content-filter; - anything else (a malformed or unexpected function call, too many tool calls,
OTHER) →error.
- a response with function calls →
costUsd is computed from the response's usage against the picked model's cost:
- uncached prompt tokens at
promptUsdPer1kTokens; - cached prompt tokens at
promptUsdPer1kTokens × cachedPromptMultiplier(default0.25, Gemini 2.5's implicit-cache discount); - completion tokens, thinking included, at
completionUsdPer1kTokens; longContext: { thresholdTokens, promptUsdPer1kTokens, completionUsdPer1kTokens }: a call whose prompt exceeds the threshold bills entirely at those rates, for a model whose price rises past a prompt size.
usage.promptTokens includes cached tokens and built-in tool prompts; usage.cachedTokens reports the cached ones when there are any.
How much it thinks
Section titled “How much it thinks”Gemini 3 models can think before they answer, as much as Google sets by default. Gemini 3.8 Flash thinks on every call, and low is the least it takes. The thinking bills as output, and it counts against maxOutputTokens, so a high level with a small cap can end with no answer.
- A call's
thinking(an agent's level, or a judge'slowest) is sent as Gemini'sthinkingLevel, upper-cased:lowestas the model'sthinking.lowest, any other level by its own name. The result'sthinkingsays the level and what was sent ({ thinkingLevel: 'LOW' }). - A call that asks none is sent the model's
metadata.models[].thinking.default, as registered. Without one, the model thinks as Google sets by default. - A level the model doesn't take goes as the next one up that it does, or its highest, with a
thinking-level-adjustedwarning. What it takes is itsthinking.levels, or without themlow,mediumandhigh(minimaltoo when its lowest isminimal): Vertex AI refusesminimalon Gemini 3.8 Flash. - A model with no
thinkinginmetadata.modelsis sent no level, with athinking-unsupportedwarning when the call asked for one. - Gemini 2.5 takes a token budget instead of a level, and isn't covered.
adapter_config.thinkingLevelsmoved tometadata.models[].thinking.defaultin 0.3.0. A new registration with it is refused, naming the new place; a stored one builds, and the key isn't read.
Exports
Section titled “Exports”createGeminiProvider(options): aModelProviderwhosemetadataisoptions.metadata.invoke()throws wheninput.modelis not one ofmetadata.models[].name.GeminiProviderOptions:metadata:ProviderMetadatawhosemodels[]areGeminiModelInfo. Caller-supplied; the API doesn't report pricing.vertex: { project, location }: the Vertex AI project, and the location (globalor a region).credentials?: () => string | Promise<string>: a service-account key, resolved on every call. The client is rebuilt only when the resolved key changes, so a rotated key applies on the next call. Absent: Application Default Credentials.client?: an injectedGeminiClient(anything withmodels.generateContent), used as is. For tests.
geminiAdapterFactory/GEMINI_ADAPTER_ID('@kindgi/adapter-model-gemini'): the factory a server registers. A provider row carries:adapter_config.project(required);metadata.regionas the location (unspecifiedmeansglobal);- an optional
secret_refholding a service-account key.
vertexTarget(input): the project and location a registration names, as the factory reads them.geminiAdapterEntry: theAdapterFactoryEntrya runtime registers,geminiAdapterFactoryplusgeminiCheckConfig(input). The check reports what the factory would throw on (adapter_config.api,secret_refon the Developer API,adapter_config.project,metadata.region) with the factory's own messages, without building anything. The entry declares the keys it takes (configKeys:api,project), so a registration with any other is refused, naming it; a stored one builds without it being read.thinkingLevelsis declared moved (movedConfigKeys), so its refusal namesmetadata.models[].thinking.default.- Cost helpers:
computeCostUsd(usage, rates),toFrameworkUsage(usageMetadata),GeminiCostRates,GeminiModelInfo,DEFAULT_CACHED_PROMPT_MULTIPLIER. - Translation helpers:
toGeminiRequest(messages),toGeminiFunctions(tools),fromGeminiResponse(response),mapFinishReason(reason),checkFunctionName(name).
Example
Section titled “Example”import { createGeminiProvider } from '@kindgi/adapter-model-gemini';
const gemini = createGeminiProvider({ vertex: { project: 'my-project', location: 'global' }, metadata: { id: 'gemini', region: 'global', models: [ { name: 'gemini-3.8-flash', contextWindow: 1_048_576, features: ['tool-use', 'structured-output', 'long-context'], maxOutputTokens: 8_192, // Rates from Google's published pricing. cost: { promptUsdPer1kTokens: 0.00075, completionUsdPer1kTokens: 0.00375 }, }, ], },});
const result = await gemini.invoke({ model: 'gemini-3.8-flash', messages: [{ role: 'user', content: 'Summarise the grievance in one sentence.' }],});With a Kindgi server, register the provider instead (kindgi providers register --spec=@provider.json): { metadata, adapter_id: '@kindgi/adapter-model-gemini', adapter_config: { project } }.
tests/translate.test.tsandtests/provider.test.tsrun against a fake client.tests/live.test.tscalls Vertex AI. It runs only whenGOOGLE_CLOUD_PROJECTis set (withGOOGLE_CLOUD_LOCATION, defaultglobal, andKINDGI_LIVE_GEMINI_MODEL, defaultgemini-2.5-flash) and Application Default Credentials are available. It covers a plain answer and a full tool round trip that sends the thought signature back.