summaryrefslogtreecommitdiffhomepage
path: root/packages/kernel/src/contracts/provider.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/kernel/src/contracts/provider.ts')
-rw-r--r--packages/kernel/src/contracts/provider.ts181
1 files changed, 109 insertions, 72 deletions
diff --git a/packages/kernel/src/contracts/provider.ts b/packages/kernel/src/contracts/provider.ts
index 52d853b..dea6c17 100644
--- a/packages/kernel/src/contracts/provider.ts
+++ b/packages/kernel/src/contracts/provider.ts
@@ -19,23 +19,23 @@ export type { ReasoningEffort, Usage } from "@dispatch/wire";
* Discriminated by `type`.
*/
export type ProviderEvent =
- | TextDeltaEvent
- | ReasoningDeltaEvent
- | ProviderToolCallEvent
- | UsageEvent
- | FinishEvent
- | ProviderErrorEvent;
+ | TextDeltaEvent
+ | ReasoningDeltaEvent
+ | ProviderToolCallEvent
+ | UsageEvent
+ | FinishEvent
+ | ProviderErrorEvent;
/** Incremental text content from the model. */
export interface TextDeltaEvent {
- readonly type: "text-delta";
- readonly delta: string;
+ readonly type: "text-delta";
+ readonly delta: string;
}
/** Incremental reasoning / thinking content from the model. */
export interface ReasoningDeltaEvent {
- readonly type: "reasoning-delta";
- readonly delta: string;
+ readonly type: "reasoning-delta";
+ readonly delta: string;
}
/**
@@ -43,16 +43,16 @@ export interface ReasoningDeltaEvent {
* dispatch to the matching `ToolContract`.
*/
export interface ProviderToolCallEvent {
- readonly type: "tool-call";
- readonly toolCallId: string;
- readonly toolName: string;
- readonly input: unknown;
+ readonly type: "tool-call";
+ readonly toolCallId: string;
+ readonly toolName: string;
+ readonly input: unknown;
}
/** Token usage report, typically emitted at step end. */
export interface UsageEvent {
- readonly type: "usage";
- readonly usage: Usage;
+ readonly type: "usage";
+ readonly usage: Usage;
}
/**
@@ -60,16 +60,16 @@ export interface UsageEvent {
* generating (e.g. "stop", "tool-calls", "length", "content-filter").
*/
export interface FinishEvent {
- readonly type: "finish";
- readonly reason: string;
+ readonly type: "finish";
+ readonly reason: string;
}
/** An error from the provider (network, rate-limit, model error, etc.). */
export interface ProviderErrorEvent {
- readonly type: "error";
- readonly message: string;
- readonly code?: string;
- readonly retryable?: boolean;
+ readonly type: "error";
+ readonly message: string;
+ readonly code?: string;
+ readonly retryable?: boolean;
}
/**
@@ -77,30 +77,43 @@ export interface ProviderErrorEvent {
* Kept minimal — providers may ignore fields they don't support.
*/
export interface ProviderStreamOptions {
- /** Model identifier to use. */
- readonly model?: string;
- /** Sampling temperature override. */
- readonly temperature?: number;
- /** Maximum output tokens override. */
- readonly maxTokens?: number;
- /** System prompt to prepend. */
- readonly systemPrompt?: string;
- /**
- * Reasoning-effort level for this request (already RESOLVED by the caller —
- * the session-orchestrator applies the request → conversation → `"high"`
- * default chain, so a provider receiving `undefined` may treat it as "no
- * preference"). The provider maps the level to its native thinking knob in
- * its own code; providers without such a knob ignore it.
- */
- readonly reasoningEffort?: ReasoningEffort;
- /**
- * Correlated logger for this turn's step (Phase A logging ABI). When present,
- * the provider should open a child `provider.request` span and capture the
- * verbatim post-transform request + raw response/error there, self-redacting
- * secrets in its own code. Optional so non-instrumented callers/tests still
- * compile (the provider falls back to no capture).
- */
- readonly logger?: Logger;
+ /** Model identifier to use. */
+ readonly model?: string;
+ /** Sampling temperature override. */
+ readonly temperature?: number;
+ /** Maximum output tokens override. */
+ readonly maxTokens?: number;
+ /** System prompt to prepend. */
+ readonly systemPrompt?: string;
+ /**
+ * Reasoning-effort level for this request (already RESOLVED by the caller —
+ * the session-orchestrator applies the request → conversation → `"high"`
+ * default chain, so a provider receiving `undefined` may treat it as "no
+ * preference"). The provider maps the level to its native thinking knob in
+ * its own code; providers without such a knob ignore it.
+ */
+ readonly reasoningEffort?: ReasoningEffort;
+ /**
+ * Correlated logger for this turn's step (Phase A logging ABI). When present,
+ * the provider should open a child `provider.request` span and capture the
+ * verbatim post-transform request + raw response/error there, self-redacting
+ * secrets in its own code. Optional so non-instrumented callers/tests still
+ * compile (the provider falls back to no capture).
+ */
+ readonly logger?: Logger;
+}
+
+/**
+ * A snapshot of the provider's current upstream usage. Returned by a
+ * provider's optional `getUsage` so a concurrency limiter can gate slot grants
+ * on the REAL upstream in-flight count (not just the limiter's local accounting,
+ * which lags the upstream `concurrent_sessions` counter by the release
+ * cooldown). `concurrentSessions` is the number of requests the provider counts
+ * as currently in flight.
+ */
+export interface ProviderUsage {
+ /** Upstream count of currently in-flight (generating) sessions. */
+ readonly concurrentSessions: number;
}
/**
@@ -110,10 +123,20 @@ export interface ProviderStreamOptions {
* is the wire model identifier; `displayName` is an optional human label.
*/
export interface ModelInfo {
- readonly id: string;
- readonly displayName?: string;
- /** The model's max context window in tokens (e.g. 200000). Optional — providers that don't report it leave it undefined. */
- readonly contextWindow?: number;
+ readonly id: string;
+ readonly displayName?: string;
+ /** The model's max context window in tokens (e.g. 200000). Optional — providers that don't report it leave it undefined. */
+ readonly contextWindow?: number;
+ /**
+ * Whether this model can natively accept image input (vision/multimodal).
+ * When `true`, image chunks in a user message are passed through to the
+ * provider serialized to its image-content format. When `false`/absent, the
+ * orchestrator's vision handoff transcribes images to text (via a
+ * vision-capable model) before the model sees them. Optional — providers
+ * that cannot detect it leave it undefined (treated as non-vision); a
+ * provider that knows a model is vision-capable sets it `true`.
+ */
+ readonly vision?: boolean;
}
/**
@@ -122,26 +145,40 @@ export interface ModelInfo {
* concrete LLM API is behind it.
*/
export interface ProviderContract {
- /** Unique identifier for this provider (e.g. "anthropic", "openai-compat"). */
- readonly id: string;
-
- /**
- * Stream a response for the given messages and available tools.
- * The provider yields `ProviderEvent`s incrementally; the kernel drives
- * tool dispatch and chunk assembly from them.
- */
- readonly stream: (
- messages: readonly ChatMessage[],
- tools: readonly ToolContract[],
- opts?: ProviderStreamOptions,
- ) => AsyncIterable<ProviderEvent>;
-
- /**
- * Enumerate the models this provider can serve, each in its own way (e.g. an
- * OpenAI-compatible provider GETs `/v1/models`). Optional: a provider that
- * cannot (or chooses not to) enumerate omits it, and a catalog simply lists
- * none for it. A future multi-credential design may pass per-credential
- * credentials in; today the provider uses the key it resolved at activate.
- */
- readonly listModels?: () => Promise<readonly ModelInfo[]>;
+ /** Unique identifier for this provider (e.g. "anthropic", "openai-compat"). */
+ readonly id: string;
+
+ /**
+ * Stream a response for the given messages and available tools.
+ * The provider yields `ProviderEvent`s incrementally; the kernel drives
+ * tool dispatch and chunk assembly from them.
+ */
+ readonly stream: (
+ messages: readonly ChatMessage[],
+ tools: readonly ToolContract[],
+ opts?: ProviderStreamOptions,
+ ) => AsyncIterable<ProviderEvent>;
+
+ /**
+ * Enumerate the models this provider can serve, each in its own way (e.g. an
+ * OpenAI-compatible provider GETs `/v1/models`). Optional: a provider that
+ * cannot (or chooses not to) enumerate omits it, and a catalog simply lists
+ * none for it. A future multi-credential design may pass per-credential
+ * credentials in; today the provider uses the key it resolved at activate.
+ */
+ readonly listModels?: () => Promise<readonly ModelInfo[]>;
+
+ /**
+ * Fetch the provider's current upstream usage snapshot. Optional: a provider
+ * that cannot (or chooses not to) report usage omits it, and a concurrency
+ * limiter falls back to cooldown-only slot recycling (no usage gate). When
+ * present, the limiter polls this before admitting a QUEUED agent and grants
+ * only when `concurrentSessions` is below the configured limit — preventing an
+ * N+1 overshoot from the upstream accounting lag.
+ *
+ * May return `undefined` (e.g. the endpoint returned an unexpected shape or a
+ * non-200) — the limiter treats `undefined` as "no usage info available" and
+ * falls back to granting (cooldown-only behavior) for that poll.
+ */
+ readonly getUsage?: () => Promise<ProviderUsage | undefined>;
}