Providers expose capabilities, not just chat.
Ling Stack should route work by capability:
chattool_callreasoningvisionweb_searchembeddingimage_generateimage_editvideo_generatettsasr
export type ProviderCapability =
| "chat"
| "tool_call"
| "reasoning"
| "vision"
| "web_search"
| "embedding"
| "image_generate"
| "image_edit"
| "video_generate"
| "tts"
| "asr";
export interface ProviderAdapter {
readonly id: string;
readonly displayName: string;
readonly capabilities: readonly ProviderCapability[];
checkHealth(): Promise<ProviderHealth>;
listModels(): Promise<readonly ModelInfo[]>;
chat?(request: ChatRequest): AsyncIterable<ChatChunk>;
webSearch?(request: WebSearchRequest): Promise<WebSearchResult>;
embed?(request: EmbeddingRequest): Promise<EmbeddingResponse>;
generateImage?(request: ImageRequest): Promise<MediaJob>;
generateVideo?(request: VideoRequest): Promise<MediaJob>;
pollJob?(jobId: string): Promise<MediaJobStatus>;
}web_search is a Ling global tool backed by provider-style backend presets. A backend preset is eligible only when Ling has an adapter that can translate its protocol into normalized evidence. Surfaces do not call adapters directly; they call the daemon web evidence service, which resolves a global web-search backend independently from the current chat provider, dispatches the adapter, and renders a bounded evidence prompt for channel or private replies.
interface WebSearchRequest {
providerPreset: ProviderPreset;
apiKey: string;
query: string;
timeoutMs?: number;
}
interface WebSearchResult {
query: string;
queries?: readonly string[];
text: string;
sources: Array<{
title?: string;
url: string;
snippet?: string;
}>;
provider: {
adapterId: string;
providerPresetId: string;
displayName: string;
};
}
interface ProviderWebSearchAdapter {
id: string;
displayName: string;
protocol: string;
supportsProvider(provider: ProviderPreset): boolean;
search(request: WebSearchRequest): Promise<WebSearchResult>;
}Current implementation:
openai-responses-web-searchsupports enabled official OpenAI Responses presets that declareweb_search, plus explicit Responses-compatible presets such as xAI/Grok or private relays withcompat.webSearch.protocol = "responses-web-search".openai-chat-completions-web-searchsupports explicit Chat Completions search presets withcompat.webSearch.protocol = "openai-chat-completions-search".google-interactions-web-searchsupports Gemini Google Search grounding presets withcompat.webSearch.protocol = "google-interactions-search".web-evidence-serviceis the global daemon surface for web intent detection, URL reading, backend-backed search dispatch, blocked reasons, and prompt metadata.web-evidence-servicemay plan a bounded set of query variants for freshness searches. The adapter still receives one query at a time and returns normalized evidence for that query.- Channel replies and private teammate replies both reuse the same evidence service; workflow steps and automations should use this service instead of adding surface-local web-search logic.
- The current chat provider does not need
web_search. Custom OpenAI-compatible, Grok, Gemini, domestic, and private models can consume global web evidence as plain context. - Generic OpenAI-compatible chat/completions relays do not inherit
web_search; each relay or vendor needs either a known protocol declaration or a dedicated adapter if it exposes search. - Domestic providers are supported through the same protocol gate when their endpoint implements a known search protocol and returns citations/sources that Ling can normalize. If a domestic endpoint uses a vendor-specific extension, add a dedicated adapter and tests before advertising
web_search. - Chat Completions citation extraction must accept URL evidence from
message.annotations,message.citations, top-levelchoice.citations, and content-partannotations/citations; compatible relays differ in which field they populate. - Perplexity/search-model providers, Anthropic, and additional domestic provider-specific search extensions remain future adapters.
Adapter quality gate:
- A provider response with source URLs is
available. - A provider response with summary text but no source URLs is
degraded; Ling can show the summary as non-authoritative context but must not present it as verified current evidence. - A provider error, missing key, or unsupported protocol is
blocked.
Supported compat.webSearch protocols:
{ "webSearch": { "protocol": "responses-web-search" } }Use for OpenAI Responses-compatible hosted search, including xAI/Grok and private relays that expose /responses with tools: [{ "type": "web_search" }].
{ "webSearch": { "protocol": "openai-chat-completions-search" } }Use for Chat Completions-compatible search models or gateways that accept web_search_options and return URL annotations.
{ "webSearch": { "protocol": "google-interactions-search" } }Use for Gemini Search grounding through the Gemini API Interactions endpoint.
endpointPath may be provided for relays that mount the same protocol at a non-standard path. It must be a relative path; full URLs are ignored so provider search cannot silently send the configured provider key to an unrelated host.
timeoutMs may be provided for slow relays. The default is 12 seconds and overrides are clamped to 12-120 seconds. This applies only to provider-backed web_search; public open_url reads keep their shorter bounded timeout.
Media execution is intentionally separate from chat provider adapters. Daemon media jobs call a MediaAdapter through executeMediaJob(adapter, request), then persist returned artifact candidates under .ling-stack/artifacts.
type MediaOperation = "image_generate" | "image_edit" | "video_generate";
interface MediaExecutionRequest {
jobId: string;
provider: MediaAdapterProviderConfig;
operation: MediaOperation;
prompt: string;
input: Record<string, unknown>;
requestedAt: string;
}
interface MediaExecutionResult {
status: "succeeded" | "failed";
artifacts: MediaExecutionArtifact[];
errorCode?: MediaAdapterErrorCode;
errorMessage?: string;
}
interface MediaAdapter {
id: string;
displayName: string;
capabilities: MediaOperation[];
supports(operation: MediaOperation): boolean;
execute(request: MediaExecutionRequest): Promise<MediaExecutionResult>;
}Current implementation:
createStaticMediaAdapter()remains the local fallback adapter,createDefaultMediaAdapterRegistry()mapsjimeng,seedream, andseedanceprovider kinds to vendor adapter factory stubs,- static vendor stubs expose vendor-specific operation capabilities and emit a redacted
static_vendor_request_plan, MediaVendorTransportis present as a disabled-by-default seam; tests can inject fake transports, and provider presetcompat.mediaVendorTransport.mode = "live"enables live transport readiness,- live transport currently supports OpenAI Images and Seedream
image_generate, Seedream URL/data-URLimage_edit, and Seedancevideo_generatewith final task polling plus video download; connected adapters record redacted JSON metadata artifacts and download returned HTTP(S) media URLs into local artifact candidates, - unknown media providers fall back to the static adapter by default, while tests can disable that fallback to assert missing-adapter errors,
- it produces redacted Markdown artifact candidates for success,
- it exposes deterministic failure/error paths for contract tests,
- it does not call Jimeng, local-file image edit materialization paths, billing, or approval services.
Ling Stack should use earendil-works/pi as the first coding-agent kernel. This is not a provider adapter in the media/model sense; it is a runner adapter that already understands coding-agent sessions, streaming, tool calls, read/write/edit/bash tools, and many LLM providers.
Keep this adapter thin:
- Use pi for agent loop and coding tools.
- Use Ling Stack for tasks, channels, approvals, audit logs, artifacts, and UI.
- Do not fork pi unless upstream blocks a required product behavior.
Run thinkingLevel accepts off, minimal, low, medium, high, xhigh, max, and ultra; CLI --thinking uses the same values. Model capabilities determine which levels are available.
maxandultraremain distinct provider-native values;maximumnormalizes tomax, neverxhigh.- Both top levels require an explicit matching
thinkingLevelMapdeclaration and a compatible runtime adapter. Missing support is rejected before dispatch. - OpenAI Chat Completions, Responses, and Codex Responses adapters can carry declared native efforts. Chat-template-only formats and explicitly disabled
reasoning_effortreject them; Anthropicmaxadditionally requires adaptive thinking, and other budget-only adapters reject both top levels. - Pi 1.1 has no
ultraslot. Ling maps its internalmaxslot to nativeultraon a session-local model copy, preserving the external request and subsequentmaxrequests.
Target capabilities:
chattool_callreasoningvisionembeddingweb_searchonly for official Responses presets with the OpenAI web-search adapter.
Target capabilities:
chattool_callreasoningvision
Target capabilities:
chattool_callwhen supported by the relay.- optional
embedding. - no implicit
web_search. - explicit
web_searchonly whencompat.webSearch.protocolnames a supported search protocol.
This adapter should support local relays like One API, New API, LiteLLM, OpenRouter-style endpoints, and private proxy services.
Target capabilities:
chattool_callwhen model support exists.reasoningwhere model support exists.web_searchthroughresponses-web-search.
Ling models Grok as an OpenAI-compatible Responses provider with an explicit web-search protocol declaration rather than adding a hidden xAI-only key path.
Target capabilities:
chattool_callwhere model support exists.visionwhere model support exists.web_searchthroughgoogle-interactions-search.
Gemini does not use OpenAI tool-call citations. Its adapter maps Google Search grounding annotations or grounding chunks into Ling's normalized source list.
Target capabilities:
chattool_callif enabled by the selected endpoint.visionwhere model support exists.web_searchonly if the configured Ark endpoint implements one of Ling's supported search protocols or a future Ark-specific adapter.
Target capabilities:
chatreasoningtool_callif endpoint support is present.web_searchonly if the configured endpoint implements one of Ling's supported search protocols or a future DeepSeek-specific adapter.
The name dpv4 should be treated as a configurable model alias until the exact upstream is pinned.
Target capabilities:
chat- future audio/video capabilities if the API key and model support them.
web_searchonly if the configured endpoint implements one of Ling's supported search protocols or a future MiniMax-specific adapter.
Target capabilities:
chattool_callembedding- coding-specific endpoint support where available.
web_searchonly if the configured endpoint implements one of Ling's supported search protocols or a future GLM-specific adapter.
Target capabilities:
image_generateimage_editvideo_generate
These should be modeled as async media jobs, not synchronous chat completions. Connected live adapters behind compat.mediaVendorTransport.mode = "live" currently cover OpenAI Images and Seedream image_generate, Seedream URL/data-URL image_edit, and Seedance video_generate after final task polling and video download. Jimeng remains fail-closed until its Visual API signing/auth contract is modeled, and local-file or stored-artifact edit materialization remains deferred.
Every adapter should have contract tests for:
- Missing API key.
- Invalid model.
- Health check.
- Streaming chat.
- Non-streaming chat if supported.
- Tool-call shape if supported.
- Rate-limit or provider error normalization.
- Secret redaction in logs.
Current media contract coverage lives in packages/providers/test/media-adapter-contract.test.mjs and covers static success, forced failure, thrown error normalization, unsupported capability rejection, provider-kind registry wiring, vendor request planning, disabled/fake/live transport seams, live transport readiness validation, Seedream image_generate mock HTTP success/failure, image URL byte download validation, vendor operation capability boundaries, missing adapter errors, non-media provider rejection, and secret redaction.
Provider errors should become a small internal set:
auth_failedmodel_not_foundrate_limitedquota_exceedednetwork_errortimeoutunsupported_capabilityprovider_internal_errorinvalid_request
Raw provider responses can be stored only after secret redaction.