← Về thư mục
📄 / / profiles / r-and-d / raw_evidence / OpenAI4S / openai4s / llm / README.md

Provider-neutral LLM layer

中文说明

The client here is pure stdlib and speaks four wires: OpenAI-compatible Chat Completions, OpenAI Responses, Anthropic Messages, and Gemini generateContent. Messages, native tool calls, streaming deltas, usage counters, and errors all come back in one normalized shape.

Where this fits

The outer loop in ../agent/ reaches models through this package and nothing else. It assembles a provider wire request and hands back normalized reply data. It does not pick the next action, run tools, start kernels, or decide that a task is finished; AgentEngine does all of that once the reply is in hand.

Capability metadata records what the OpenAI4S adapter supports today. It says nothing about the full feature set a vendor exposes through its own SDK.

Files

File Responsibility
__init__.py The package facade, and the reason the module-to-package split did not break anything. It re-exports configuration, capabilities, the registry, LLMError, and chat. _post_json and _post_sse stay module globals on purpose: replacing those two names is how the offline tests and other integrations intercept the wire. They forward a call context — which provider this is, and whether the user has since pressed stop — through bind_call_context, which binds only the keywords the target actually accepts, so the plain four-argument transports those integrations inject keep working instead of raising TypeError on a call that would have succeeded.
capabilities.py What each provider and model is declared to support. A provider baseline, a deployment override, and an exact-model override resolve into one cached record, and validate_model_request refuses a request for a feature the model was never declared to have instead of letting the wire fail on it. The same record maps a vendor's usage fields onto canonical token counts, and cost is estimated from those. Overrides are process-local; this module touches no files.
catalog.py Model-profile presets, thread-safe and process-local. It knows nothing about wires.
client.py The one provider-neutral entry point. It turns configuration plus the provider spec into a base URL and a model, refuses image parts addressed to a text-only provider, and hands the request to the registered wire adapter. If the resolved model does not declare tool calling, the native declarations are dropped rather than sent — the turn falls back to the Code-as-Action path instead of failing on an unsupported schema. A missing API key is fatal only for a remote endpoint; a local one authenticates by being unreachable from anywhere else. supports_vision_for answers for the exact provider/endpoint/model triple a call would use, because the per-provider answer is about that provider's default model: a session pinned to a text-only model on a vision-capable provider passed the pre-flight and was then refused by the guard below it, turning a graceful text fallback into a failed turn. Usage is normalized on the way back.
messages.py History translation, one function per wire. OpenAI keeps system as an ordinary message; Anthropic, Gemini, and Responses want it lifted out, and want consecutive tool results folded into a single turn. Native calls, tool results, and multipart image content all cross here without losing their original arguments.
models.py Defines LLMError, the single failure type every transport and provider raises, and TransportError under it: an HTTP failure with its evidence intact — status, provider error code, headers, request id, Retry-After, and the output_committed flag that vetoes a retry no matter how retryable the status looks. Every failure used to be flattened into an f-string, so a 429 could not be told from a 401 without parsing English and nothing retried. It subclasses LLMError, so existing except LLMError handlers keep working and callers that want the detail opt in. llm_failure_code maps only an exact, closed set of structured provider signals onto local public codes; it never classifies from provider prose or publishes a raw provider code. status_is_retryable and parse_retry_after (a delta or an HTTP-date, never negative) live here with it.
registry.py Which providers exist in this process, and what each one is: wire, base URL, API-key environment variable, default model, capability bindings. Registration is validated (an absolute http(s) base_url, no credentials embedded in it) and a built-in provider can be neither replaced nor removed.
tooling.py The native-tool contract, kept here so no wire adapter has to import the tool registry. Declarations are canonicalized to one name/description/schema form, then rendered into each wire's tool schema and tool choice. Calls coming back are normalized to a shared shape; arguments that fail to decode are carried as a parse_error on the call rather than discarded.
transport.py The only place in the package that opens a socket: JSON POST and SSE decoding over urllib, no provider SDK. HTTP and connection failures surface as TransportError, which is what makes a retry possible at all. Structured error events carried inside an HTTP-200 SSE response keep that same typed path. The policy is deliberately narrow: only a request that committed nothing may be replayed — a whole-response POST can be, a stream that already handed events to the caller cannot — with one bounded attempt budget, exponential jitter, Retry-After beating the computed delay, cancellation polled during the wait rather than only on either side of it, and a total budget so a five-minute Retry-After cannot silently park a turn. Burst-protection failures use a slower jitter window with a positive floor; an exhausted stream failure does not start a fresh blocking retry budget. A non-empty stream event that is not valid JSON is raised rather than skipped, because a dropped event could have been a tool call; the offending chunk is truncated in the error text.
resolve.py One answer to "which model, which key, which endpoint", used by the request path and by openai4s doctor alike. Process config is layered with the Customize -> Models settings held in the store and then with the model the session itself pinned, so a diagnostic cannot report a setup as broken that a real turn resolves fine — the daemon boots with no key on purpose. Overriding the provider clears the previous one's concrete base URL, model and key instead of inheriting them, or the request goes to the wrong endpoint under the wrong credential. is_loopback_endpoint also lives here: Ollama, LM Studio, vLLM and llama.cpp all speak the OpenAI-compatible wire on loopback and authenticate by being unreachable from anywhere else, so demanding a key from them reported a working install as broken — and only a literal loopback address counts, because a hostname that resolves to 127.0.0.1 today is not a stable authorisation story.

Subdirectories

Directory Responsibility
providers/ The wire adapters themselves: OpenAI-compatible Chat, Responses, Anthropic Messages, and Gemini generateContent.

Provider extension contract