SPEC: agent-provider
Overview
The official AI provider implementations of the Robota SDK in one package, one subpath per vendor:
./anthropic, ./openai, ./gemini (with ./google, a deprecated alias), ./bytedance, ./deepseek,
./qwen, ./gemma and ./builtin. ./openai-compatible is the OpenAI wire-protocol base that ./openai,
./deepseek, ./qwen and ./gemma share, and that the author of another OpenAI-compatible provider can build on.
The contracts a provider implements (IAIProvider, AbstractAIProvider, provider definitions and capability
types) stay in @robota-sdk/agent-core; this package does not re-export them. A user who needs a provider not
included here implements IAIProvider from @robota-sdk/agent-core (or extends AbstractAIProvider, or builds on
./openai-compatible) and registers it directly.
Contract
- There is no root export. Each vendor is its own subpath, so importing one vendor never loads another vendor's code or SDK.
- The vendor SDKs (
openai,@anthropic-ai/sdk,@google/genai) are optional peer dependencies: an application installs only the SDKs of the providers it imports../bytedanceuses a bespoke HTTP client and needs none. - The package declares
sideEffects: false. The one import-time effect is deliberate and safe under that flag: a Claude model module registers its models with the core registry, and only the code that uses those models imports it. - Among framework packages this package depends on
@robota-sdk/agent-coreonly;agent-framework,agent-sessionand every higher-layer package must never be imported. A vendor subpath imports no other vendor subpath, except that./openai,./deepseek,./qwenand./gemmabuild on./openai-compatibleand./builtincomposes the others;./builtinbelongs at a composition root.
Design decisions
- One package with subpaths, no facade. An earlier single package re-exported every vendor from its root and hard-depended on all three SDKs, so any importer installed them all and could bundle them all. Separate packages fixed that but left the OpenAI protocol base without a parent to hold it. Subpaths plus optional peers keep a one-vendor import the size of that vendor, with the shared base in the same package.
Anthropic (./anthropic)
Overview
Anthropic Claude provider implementation, built on the @anthropic-ai/sdk.
Module ordering
Within the package, the shared source-URL/verification-date constants used by both the provider definition and the capability table have a single owner module, and the definition re-exports them rather than the capability table depending on the definition — so importing the definition first as native ESM cannot read an uninitialized value through that path.
Diagnostic endpoint
The provider definition declares a diagnostic endpoint (the vendor SDK's embedded host/port), stated on the agent runtime side purely for the pre-session doctor's TCP reachability check. It is deliberately not the runtime-effective base URL: that field is persisted into created profiles and passed to provider construction, while the diagnostic endpoint is read by no setup, persistence, or provider-construction path. A profile that sets its own base URL is probed at that host instead.
Model Effort
The provider exposes a source-dated effort table only at the vendor endpoint. For a documented model,
a concrete Core effort selection is serialized as a native effort field and merged with an existing
output-format setting; auto reports the table default while omitting the native effort field. An
unknown model, or a configured non-default base URL, reports "not applied" and sends no unverified
control. A terminal observer receives exactly one serializable resolution/native-control/dispatch
outcome after success.
Tool Schema Projection
The provider's schema projection profile is permissive: Anthropic accepts standard JSON Schema with an
object root, so no keyword stripping or object closure runs before a tool schema is handed to the
vendor SDK's conversion. A tool whose schema is rejected by projection (a non-object root, a
prototype-key property name, a cycle, or a schema over the shared depth/node ceiling) is omitted from
that request alone — every other tool on the turn is unaffected — and is reported once per cache
identity through the shared tool-schema-projection logger. When a call selects toolChoice: none,
the provider also omits its configured server-side web search, for both ordinary and streaming
calls, so a server tool cannot still execute when the core passed no local tool schemas.
Server-side web search
Whether the provider attaches Anthropic's server-side web search is fixed when it is built, from the profile option the definition declares; nothing enables it afterwards, so a session or a provider swap cannot change what a provider runs. A malformed option fails at construction, because a misspelled key that quietly left search off would look like a working configuration.
OpenAI (./openai)
Overview
The OpenAI provider (openai SDK) requests generation policy without hiding returned calls or guaranteeing result validity; callers own call-count and argument validation, and hosts own tool execution. Extra request-body fields a caller supplies are forwarded unchanged and are the caller's to interpret: they can never replace a field the provider writes or one its response parsing depends on, and the provider's own reports (effort outcome, capabilities) describe only what it writes. They belong to the provider's construction, so a configuration that would route them through an executor, whose contract carries no request body, is refused rather than dropped. The OpenAI-compatible protocol base lives in this package, in ./openai-compatible, and ./openai imports it directly.
Design Decisions
Diagnostic endpoint vs. runtime base URL
The provider definition states the vendor SDK's default endpoint host/port on the agent runtime side purely for the pre-session doctor's TCP reachability check. This is deliberately kept separate from the runtime-effective base URL (the one persisted into profiles and passed to provider construction): a profile that sets its own base URL is probed at that host instead, not the vendor default.
Reasoning effort is verified per-model, not assumed
The framework threads a per-call reasoning-effort selection through chat options. This package owns a source-dated exact-model table and applies native effort control only for the official Responses endpoint against a verified model. auto resolves to the documented model default but omits the native effort field entirely. An unknown model, a Chat Completions surface, or a custom base URL is reported not-applied and emits no native control — support is never inferred from protocol compatibility alone.
When a selection resolves to auto/model-default or not-applied, a statically-set native effort field is treated as a conflict and rejected: silently keeping it would send a native control while reporting that none was sent.
Tool schema closure runs at the projection seam, not in the converters
Schema rewriting for OpenAI's strict tool mode happens in the provider's projection-profile seam (shared with agent-core's schema projector), not inside the Responses/Chat Completions request converters. The converters only shape an already-projected schema into the wire tool definition.
Why: OpenAI strict mode requires every object node (nested included) to carry additionalProperties: false and list all its properties in required. A Zod-derived schema does not guarantee either by default, so without a closure step, most Zod-defined tools would be rejected once strict mode is enabled.
The lossy part, stated as a contract guarantee: strict mode cannot express "optional", so an optional property is forced into required and compensated with a nullable union. A forced-optional field and a genuinely nullable one are therefore indistinguishable on the wire — this is a known, accepted limitation, not a bug. The model must supply the key; null means "not provided". A handler that needs to distinguish an absent key from an explicit null cannot rely on this provider to preserve that distinction.
Because the rewrite is lossy, it only runs when strict mode is actually being sent. With strict tools off, tool parameter schemas are forwarded as declared, except that a bare array gains items and a bare object properties: the API refuses those in either mode, and the declared schema still governs local validation.
A tool schema the projector rejects (non-object root, a prototype-key property name, a cycle, or a schema exceeding the shared depth/node ceiling) is dropped from that request alone rather than failing the whole call, and is logged once per cache identity so the omission is auditable.
No fabricated capability table
This package declares no per-model capability table for OpenAI, since none has been verified — inventing one would be a fabricated claim. The framework's miss policy already handles this silence correctly: a provider that declares nothing is sent a structured request unchanged.
It does declare whether the configured endpoint is the vendor default, as a boolean rather than a capability-table field, so a provider with no table can still answer that question. This matters because setting a custom base URL also switches the API surface to Chat Completions, which is the surface least likely to honor a structured-output parameter; a structured request through such a gateway is reported with an "unverified endpoint" provenance rather than being claimed as enforced.
Log file permissions
The file-based payload logger writes a summary of each Chat Completions request — request metadata, not prompt or response content — to a caller-supplied log directory. It creates that directory and each log file with owner-only permissions rather than inheriting the process umask, since the directory is caller-supplied and the files record the caller's own request history.
A searching reply goes back as OpenAI's items, only within its turn
Hosted search results exist only on OpenAI's side, named by a reply's output items, so a reply that searched and called functions is sent back whole on the later requests of its turn that carry the search tool. previous_response_id is not used: the conversation held here, which compaction can rewrite, stays what the model is given. Those items belong to the account and model that made them, OpenAI keeps them for a limited time, and it has none for an unstored response, so only the provider instance that recorded a reply sends it back, on the same model, until the next user message, and only on a request that declares the tool, since OpenAI backends are reported to refuse search items otherwise. Everything else is rebuilt from text, because losing the search costs less than failing: a fallback to another model or account, a resumed session and the forced summary must not fail on items they cannot use.
Gemini (./gemini and ./google)
Purpose
Google Gemini provider implementation (@google/genai), also implementing IImageGenerationProvider.
A deprecated GoogleProvider compatibility alias is re-exported via the ./google entry for callers
migrating off the old name.
Contract
- Streaming.
GeminiProviderpreserves every assistant function call andusageMetadatavalue returned by Gemini's streaming API.chatStream()emits text deltas as they arrive plus a universal assistant message for a function-call chunk or a usage-only terminal chunk. Whenchat()uses its streaming assembly path (onTextDeltaset), it returns one complete assistant message assembled from all stream chunks. - Requests containing
nativeWebToolsare validated by bothchat()andchatStream(), so a request that requires a hosted tool this provider is not running fails explicitly instead of being silently ignored. Google Search is a construction choice and rides only on requests that can carry it: a model that cannot search next to function tools keeps the function tools, because an agent's work needs them. - Model parts return as they came. A reply's signed function calls and server-side tool steps reach the next request verbatim through the provider continuation, because Gemini refuses a signed call without its signature and expects those steps echoed. The steps go wherever the model can run them, whatever that request's tools, because a reply with its context cut can be refused. They stay out in Vertex mode, where the SDK refuses them, and on a model that cannot combine tools. If the parts no longer say what the message says, the message wins and they are rebuilt from it.
- Model effort. A verified model selection sends Gemini's
thinkingConfig.thinkingLevelcontrol, preserves unrelatedthinkingConfigfields, and rejects a conflicting staticthinkingLevelorthinkingBudget.autoreports the documented default while omitting a control; unknown models and unverified routes reportnot-appliedrather than inventing a numericthinkingBudgetmapping. - Tool schema projection.
GeminiProvider.projectionProfile()strips foreign JSON-Schema keywords andadditionalPropertiesbefore tool schemas reach Gemini's request builder, because Gemini'sSchematype is a fixed OpenAPI-3.0 subset, not standard JSON Schema — a member the builder doesn't understand must not fail silently downstream. The builder then avoids what that subset refuses although JSON Schema allows it: a null type, which becomesnullable, arequiredname with no property, and an emptyparametersor nestedproperties. A tool that projection rejects is omitted from that request alone and reported once per cache identity vian agent-core'sToolSchemaProjectionlogger.
Design decisions
- The provider definition's diagnostic
endpoint(generativelanguage.googleapis.com:443) is stated separately fromdefaults.baseURLon purpose:endpointexists only for the pre-session doctor's TCP reachability check, whilebaseURLis the runtime-effective value persisted into profiles and passed tocreateProvider. A profile with its ownbaseURLis probed at that host instead.
DeepSeek, Qwen and Gemma, and the OpenAI-compatible protocol base (./deepseek, ./qwen, ./gemma and ./openai-compatible)
Purpose
OpenAI-compatible providers (DeepSeek, Qwen, Gemma) and the shared protocol implementation used by
them and by ./openai preserve admitted observations, call identity
and host source attribution without treating the wire role of native vision data as contributor
authority.
Executor delegation
When a DeepSeek, Qwen, or Gemma provider is configured with an executor, the provider unwraps the executor's non-streaming result and yields only the message stream events. The executor owns the terminal event and its effort observer; providers do not expose the terminal envelope as a provider message or invoke the observer a second time.
Structured output
No adapter in this package reads a native response-format option directly — deepseek, qwen,
and gemma build their requests without it. Structured-output handling lives at the core transport
seam instead: agent-core asks this package's capability table which transport applies and, when no
schema parameter exists, states the schema as a system instruction on the first attempt, so a
structured run against this family does not spend an extra turn finding out the shape. The mapping
lives at the seam that assembles the request rather than being duplicated into three request
builders, so the two answers cannot drift apart.
The capability declarations say what each model really does: DeepSeek's JSON Output mode guarantees the response parses but takes no schema parameter and enforces no shape, so it is declared as an unstructured JSON capability rather than a schema-enforcing one. Qwen declares neither, so its structured requests omit the option entirely rather than sending one to be ignored.
agent-core's bounded validate-and-retry loop remains the guarantee: the returned object matches the schema or it throws.
Tool schema projection
gemma, qwen, and deepseek each use the same permissive tool-schema profile from
@robota-sdk/agent-core, differing only in provider name — every sibling in this family accepts
standard JSON Schema, so no keyword stripping or object closure runs. Each provider projects tools
before handing options to the shared request builder, so the request that builder sees always
already carries projected tools; Qwen's Responses surface (live when built-in web tools are on)
projects the same way before building its own request. Qwen's Responses surface also omits its
configured server-side web search/extractor tools whenever a call selects toolChoice: none, for
both direct and streaming responses, so a text-only call cannot acquire hosted web actions merely
because the provider instance has them enabled.
A tool schema the projector rejects is omitted from that request alone and reported once per cache identity via a dedicated log line on agent-core's global-sink schema-projection logger — audible even though these providers construct with a silent logger by default.
ByteDance (./bytedance)
Purpose
Bytedance (ModelArk) video-generation provider implementing IVideoGenerationProvider. Uses a bespoke HTTP client — no vendor SDK.
Contract
- Implements
IVideoGenerationProviderfrom@robota-sdk/agent-core.
Built-in definitions (./builtin)
Purpose
Composition leaf that aggregates the built-in provider definitions a host composes: the chat
provider definitions, the media provider definitions (image and video generation), and a default
per-role model map (DEFAULT_ROLE_MODELS) — an app-workflow opinion that the neutral role-model
contract in agent-core deliberately does not embed. Video generation is a media provider, so the
chat definitions leave it out.