@cleocode/contracts 2026.5.65 → 2026.5.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/index.d.ts +6 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/llm/failover-reason.d.ts +76 -0
  5. package/dist/llm/failover-reason.d.ts.map +1 -0
  6. package/dist/llm/failover-reason.js +2 -0
  7. package/dist/llm/failover-reason.js.map +1 -0
  8. package/dist/llm/interfaces.d.ts +476 -0
  9. package/dist/llm/interfaces.d.ts.map +1 -0
  10. package/dist/llm/interfaces.js +24 -0
  11. package/dist/llm/interfaces.js.map +1 -0
  12. package/dist/llm/normalized-response.d.ts +304 -0
  13. package/dist/llm/normalized-response.d.ts.map +1 -0
  14. package/dist/llm/normalized-response.js +19 -0
  15. package/dist/llm/normalized-response.js.map +1 -0
  16. package/dist/llm/provider-id.d.ts +47 -0
  17. package/dist/llm/provider-id.d.ts.map +1 -0
  18. package/dist/llm/provider-id.js +13 -0
  19. package/dist/llm/provider-id.js.map +1 -0
  20. package/dist/llm/provider-profile.d.ts +166 -0
  21. package/dist/llm/provider-profile.d.ts.map +1 -0
  22. package/dist/llm/provider-profile.js +13 -0
  23. package/dist/llm/provider-profile.js.map +1 -0
  24. package/dist/llm/resolved-credential.d.ts +61 -0
  25. package/dist/llm/resolved-credential.d.ts.map +1 -0
  26. package/dist/llm/resolved-credential.js +14 -0
  27. package/dist/llm/resolved-credential.js.map +1 -0
  28. package/dist/operations/llm.d.ts +14 -2
  29. package/dist/operations/llm.d.ts.map +1 -1
  30. package/dist/operations/llm.js +4 -0
  31. package/dist/operations/llm.js.map +1 -1
  32. package/package.json +10 -2
  33. package/src/index.ts +43 -0
  34. package/src/llm/failover-reason.ts +86 -0
  35. package/src/llm/interfaces.ts +538 -0
  36. package/src/llm/normalized-response.ts +313 -0
  37. package/src/llm/provider-id.ts +63 -0
  38. package/src/llm/provider-profile.ts +191 -0
  39. package/src/llm/resolved-credential.ts +62 -0
  40. package/src/operations/llm.ts +20 -2
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Normalized provider response types for the CLEO LLM transport layer.
3
+ *
4
+ * Ported from Hermes `agent/transports/types.py` (Python dataclasses →
5
+ * TypeScript interfaces, snake_case → camelCase). These types define the
6
+ * canonical shape that all provider transports normalize responses to. The
7
+ * shared surface is intentionally minimal — only fields that every downstream
8
+ * consumer reads are top-level. Protocol-specific state goes in `providerData`
9
+ * dicts so that protocol-aware code paths can access it without polluting the
10
+ * shared type.
11
+ *
12
+ * @module llm/normalized-response
13
+ * @task T9263
14
+ * @task T9282 (W0c — Phase 4 multimodal + stream extensions)
15
+ * @epic T-LLM-CRED-CENTRALIZATION
16
+ * @see ADR-072 §LlmTransport — pure wire level
17
+ */
18
+
19
+ import type { ModelTransport } from '../operations/llm.js';
20
+ import type { NormalizedDelta, TransportContext } from './interfaces.js';
21
+ import type { ApiMode } from './provider-id.js';
22
+
23
+ /**
24
+ * Token usage reported by the provider for a single API call.
25
+ *
26
+ * `cachedTokens` is only populated when the provider supports prompt-cache
27
+ * read accounting (Anthropic extended caching, Gemini cached content).
28
+ */
29
+ export interface NormalizedUsage {
30
+ /** Tokens consumed by the prompt. */
31
+ inputTokens: number;
32
+ /** Tokens generated by the model. */
33
+ outputTokens: number;
34
+ /** Tokens served from prompt cache (if provider supports caching). */
35
+ cachedTokens?: number;
36
+ }
37
+
38
+ /**
39
+ * A single normalized tool call emitted by the model.
40
+ *
41
+ * `id` is the protocol-canonical identifier used when constructing tool result
42
+ * messages. May be `null` when the provider omits it (rare in practice but
43
+ * possible with some adapters).
44
+ *
45
+ * `providerData` carries per-tool-call protocol metadata that only
46
+ * protocol-aware code reads. Examples:
47
+ * - Codex: `{ call_id: "call_XXX", response_item_id: "fc_XXX" }`
48
+ * - Gemini: `{ extra_content: { thought_signature: "..." } }`
49
+ */
50
+ export interface NormalizedToolCall {
51
+ /** Provider-assigned tool-call id (may be null if provider does not surface one). */
52
+ id: string | null;
53
+ /** Tool name as declared in the request. */
54
+ name: string;
55
+ /** Arguments as a JSON string (provider-agnostic). */
56
+ arguments: string;
57
+ /** Provider-specific extras (raw shape, do not depend on). */
58
+ providerData?: Record<string, unknown>;
59
+ }
60
+
61
+ /**
62
+ * Normalized API response from any LLM provider.
63
+ *
64
+ * Shared fields are truly cross-provider — every caller can rely on them
65
+ * without branching on transport. Protocol-specific state goes in
66
+ * `providerData` so that only protocol-aware code paths read it.
67
+ *
68
+ * `raw` is the unmodified SDK response object. Consumers MUST narrow it with
69
+ * a type guard before accessing fields. NEVER log `raw` without scrubbing
70
+ * auth headers.
71
+ */
72
+ export interface NormalizedResponse {
73
+ /** Response id from the provider (or a synthesized id if absent). */
74
+ id: string;
75
+ /** Model identifier as reported by the provider. */
76
+ model: string;
77
+ /** Plain text content (null when the model returned only tool calls). */
78
+ content: string | null;
79
+ /** Tool calls, or null when none. */
80
+ toolCalls: NormalizedToolCall[] | null;
81
+ /**
82
+ * Provider-reported stop reason.
83
+ * Common values: `'end_turn'`, `'tool_use'`, `'stop'`, `'max_tokens'`,
84
+ * `'stop_sequence'`, `'tool_calls'`, `'length'`.
85
+ */
86
+ stopReason: string;
87
+ /** Token usage for this call. */
88
+ usage: NormalizedUsage;
89
+ /**
90
+ * Optional model-internal reasoning trace.
91
+ * Populated for Anthropic extended-thinking and OpenAI o1-series models.
92
+ */
93
+ reasoning?: string | null;
94
+ /** Provider-specific response-level extras (raw shape, do not depend on). */
95
+ providerData?: Record<string, unknown>;
96
+ /**
97
+ * Raw provider response — DO NOT log this without scrubbing auth headers.
98
+ *
99
+ * Typed as `unknown` because the concrete SDK response shapes
100
+ * (Anthropic.Message, OpenAI.ChatCompletion, …) are not imported from
101
+ * contracts. Consumers MUST narrow with a type guard before accessing fields.
102
+ */
103
+ raw: unknown;
104
+ }
105
+
106
+ /**
107
+ * An image content block for multimodal messages.
108
+ *
109
+ * `source.type` determines how the image data is provided:
110
+ * - `'base64'` — raw image bytes encoded as base64, with `mediaType` identifying
111
+ * the MIME type (e.g. `'image/png'`, `'image/jpeg'`).
112
+ * - `'url'` — a publicly accessible URL; `data` is the URL string and `mediaType`
113
+ * is still populated (sniffed by {@link image-routing.ts} when not supplied by
114
+ * the caller).
115
+ *
116
+ * @see ADR-072 §LlmTransport — pure wire level (W4d image routing)
117
+ */
118
+ export interface TransportImageBlock {
119
+ /** Discriminant — always `'image'`. */
120
+ readonly type: 'image';
121
+ readonly source: {
122
+ /** How the image data is provided. */
123
+ readonly type: 'base64' | 'url';
124
+ /**
125
+ * Base64-encoded image bytes (when `type === 'base64'`) or a URL string
126
+ * (when `type === 'url'`).
127
+ */
128
+ readonly data: string;
129
+ /** MIME type, e.g. `'image/png'`, `'image/jpeg'`, `'image/webp'`. */
130
+ readonly mediaType: string;
131
+ };
132
+ }
133
+
134
+ /**
135
+ * A text content block for multimodal messages.
136
+ */
137
+ export interface TransportTextBlock {
138
+ /** Discriminant — always `'text'`. */
139
+ readonly type: 'text';
140
+ /** Plain text string. */
141
+ readonly text: string;
142
+ }
143
+
144
+ /**
145
+ * A single message in a provider-neutral conversation turn.
146
+ *
147
+ * `toolUseId` is required when `role` is `'tool'` — it identifies which
148
+ * tool call this result resolves, matching {@link NormalizedToolCall.id}.
149
+ *
150
+ * `content` supports both the legacy plain-string form and a multimodal block
151
+ * array. When `content` is a string, the transport treats it as a single text
152
+ * block. When it is an array, each element is either a {@link TransportTextBlock}
153
+ * or a {@link TransportImageBlock} — enabling image routing (W4d) without
154
+ * breaking existing transport consumers that only read the string form.
155
+ */
156
+ export interface TransportMessage {
157
+ /** Conversation turn role. */
158
+ role: 'user' | 'assistant' | 'tool';
159
+ /**
160
+ * Message content.
161
+ *
162
+ * - Plain `string` — backwards-compatible single-text-block form used by
163
+ * all existing transports. Transports that do not support images receive
164
+ * this form only.
165
+ * - `ReadonlyArray<TransportTextBlock | TransportImageBlock>` — multimodal
166
+ * block array enabling image routing (ADR-072 W4d). Transports that do not
167
+ * support images MUST stringify text blocks and drop image blocks (or throw
168
+ * if `imageMode` on the request is `'native'` — see {@link TransportRequest}).
169
+ */
170
+ readonly content: string | ReadonlyArray<TransportTextBlock | TransportImageBlock>;
171
+ /** Tool-result messages: id of the call this resolves. */
172
+ toolUseId?: string;
173
+ }
174
+
175
+ /**
176
+ * A tool definition passed to the provider.
177
+ *
178
+ * `inputSchema` is a JSON Schema object describing the tool's input parameters.
179
+ * The transport layer passes it through verbatim after any provider-specific
180
+ * adaptation (e.g. Anthropic wraps it in `{ type: "object", ... }`).
181
+ */
182
+ export interface TransportTool {
183
+ /** Tool name as it will appear in {@link NormalizedToolCall.name}. */
184
+ name: string;
185
+ /** Human-readable description for the model. */
186
+ description: string;
187
+ /** JSON Schema for input parameters. */
188
+ inputSchema: Record<string, unknown>;
189
+ }
190
+
191
+ /**
192
+ * Request parameters passed to {@link LlmTransport.complete} and
193
+ * {@link LlmTransport.stream}.
194
+ *
195
+ * `temperature` is normalized to the 0.0–1.0 range; each transport clamps it
196
+ * to the provider's supported range before sending.
197
+ *
198
+ * `signal` may be used to abort in-flight requests (passed through to the
199
+ * underlying fetch / SDK call where supported).
200
+ *
201
+ * @see ADR-072 §LlmTransport — pure wire level
202
+ */
203
+ export interface TransportRequest {
204
+ /** Model identifier — provider-specific (e.g. `'claude-sonnet-4-6'`). */
205
+ model: string;
206
+ /** Conversation messages in provider-neutral form. */
207
+ messages: TransportMessage[];
208
+ /** Maximum tokens to generate (NOT input budget). */
209
+ maxTokens: number;
210
+ /** Optional system prompt (Anthropic top-level `system`) or system message (OpenAI). */
211
+ system?: string;
212
+ /** Optional tools the model may call. */
213
+ tools?: TransportTool[];
214
+ /** Sampling temperature in 0.0–1.0 range; provider clamps to its own range. */
215
+ temperature?: number;
216
+ /** AbortSignal for request cancellation. */
217
+ signal?: AbortSignal;
218
+ /**
219
+ * Prompt-cache breakpoint injection strategy.
220
+ *
221
+ * - `'system_and_3'` — inject cache breakpoints after the system prompt and
222
+ * after the last 3 user messages (Anthropic extended-caching strategy).
223
+ * - `'prefix_and_2'` — inject at the shared prefix boundary and after the 2
224
+ * most-recent turns (optimised for Gemini cached content).
225
+ * - `null` — no cache injection (default when not set).
226
+ *
227
+ * Transports apply the selected strategy before constructing the SDK request.
228
+ * Unsupported strategies on a given transport are silently ignored.
229
+ *
230
+ * @see ADR-072 §LlmTransport — pure wire level
231
+ */
232
+ readonly cacheStrategy?: 'system_and_3' | 'prefix_and_2' | null;
233
+ /**
234
+ * Image handling mode for multimodal content blocks in {@link TransportMessage.content}.
235
+ *
236
+ * - `'native'` — send image blocks as-is to the provider's multimodal API.
237
+ * The transport MUST throw if the provider does not support native images.
238
+ * - `'text'` — strip all image blocks; only text blocks are sent. Useful for
239
+ * text-only providers or when images are redundant.
240
+ * - `'auto'` — the transport decides based on provider capability (default).
241
+ * Falls back to `'text'` for providers that do not support native images.
242
+ *
243
+ * Used by the image-routing layer (W4d) to select the per-turn input mode.
244
+ *
245
+ * @see ADR-072 §LlmTransport — pure wire level
246
+ */
247
+ readonly imageMode?: 'native' | 'text' | 'auto';
248
+ }
249
+
250
+ /**
251
+ * Provider transport contract.
252
+ *
253
+ * Each concrete transport (Anthropic, OpenAI, Gemini) implements this
254
+ * interface and maps provider-specific API shapes to/from
255
+ * {@link NormalizedResponse}. Role-resolver and executor code works
256
+ * exclusively against this interface so providers are swappable.
257
+ *
258
+ * As of Phase 4 (ADR-072) the interface gains two additional members:
259
+ * - {@link apiMode} — the wire protocol spoken by this transport.
260
+ * - {@link stream} — streaming completion returning {@link NormalizedDelta} chunks.
261
+ *
262
+ * Existing transport implementations MUST add stub implementations of both to
263
+ * maintain compile parity (W0c). Wave 1 migrations replace stubs with real impls.
264
+ *
265
+ * @see ADR-072 §LlmTransport — pure wire level
266
+ */
267
+ export interface LlmTransport {
268
+ /**
269
+ * Provider identifier — matches the `ModelTransport` union so role-resolver
270
+ * can select the correct transport at runtime.
271
+ */
272
+ readonly provider: ModelTransport;
273
+ /**
274
+ * Wire protocol this transport speaks. Used by the session and executor layers
275
+ * to select correct transports for providers that support multiple protocols.
276
+ *
277
+ * @see ADR-072 §Type lock-in — `ApiMode` closed 4-value union
278
+ */
279
+ readonly apiMode: ApiMode;
280
+ /**
281
+ * Execute a single completion call and return a normalized response.
282
+ *
283
+ * Implementations MUST:
284
+ * - Never return raw credentials in the response envelope.
285
+ * - Preserve the full provider response in {@link NormalizedResponse.raw}.
286
+ * - Map provider-specific stop reasons to the canonical set where possible.
287
+ *
288
+ * @param request - Provider-neutral request parameters.
289
+ * @param ctx - Contextual metadata (request ID, abort signal, feature flags).
290
+ * @returns A promise that resolves to a {@link NormalizedResponse}.
291
+ */
292
+ complete(request: TransportRequest, ctx?: TransportContext): Promise<NormalizedResponse>;
293
+ /**
294
+ * Stream a completion. Each {@link NormalizedDelta} carries incremental text
295
+ * or reasoning content.
296
+ *
297
+ * Implementors MUST run streaming deltas through `StreamingThinkScrubber`
298
+ * before yielding them — reasoning content goes to `delta.reasoning`;
299
+ * visible text goes to `delta.text`. The final delta has a non-null
300
+ * `stopReason` and carries full usage stats in `delta.usage`.
301
+ *
302
+ * W0c transports carry a stub that throws until Wave 1 migration lands:
303
+ * ```ts
304
+ * throw new Error('STUB: W1 migration will implement stream() for <provider>');
305
+ * ```
306
+ *
307
+ * @param request - Provider-neutral request parameters.
308
+ * @param ctx - Contextual metadata (request ID, abort signal, feature flags).
309
+ * @returns An async iterable of normalized delta chunks.
310
+ * @see ADR-072 §LlmTransport — pure wire level
311
+ */
312
+ stream(request: TransportRequest, ctx: TransportContext): AsyncIterable<NormalizedDelta>;
313
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Provider identity and API protocol types for the unified LLM provider architecture.
3
+ *
4
+ * These types anchor the Phase 4 contract layer (ADR-072). All downstream code
5
+ * that references a provider by name or by wire-protocol uses these types.
6
+ *
7
+ * @module llm/provider-id
8
+ * @task T9281
9
+ * @epic T9261 T-LLM-CRED-CENTRALIZATION
10
+ * @see ADR-072 §Type lock-in
11
+ */
12
+
13
+ /**
14
+ * Canonical wire-level API protocol supported by a provider.
15
+ *
16
+ * Closed 4-value union. Providers that speak multiple protocols (e.g. OpenAI
17
+ * supports both chat_completions and codex_responses) declare separate
18
+ * ProviderProfiles, one per ApiMode.
19
+ *
20
+ * Adding a fifth value is a major breaking change — confirm in ADR-072 before doing so.
21
+ *
22
+ * @see ADR-072 §Type lock-in
23
+ */
24
+ export type ApiMode =
25
+ | 'chat_completions' // OpenAI, OpenRouter, Groq, DeepSeek, Moonshot, xAI, Gemini-via-OR
26
+ | 'anthropic_messages' // Native Anthropic SDK — full prompt-cache + thinking
27
+ | 'codex_responses' // OpenAI Responses API (Codex CLI, xAI-responses)
28
+ | 'bedrock_converse'; // AWS Bedrock ConversationAPI
29
+
30
+ /**
31
+ * Fixed set of builtin provider identifiers shipped with CLEO core.
32
+ *
33
+ * Plugin providers registered at runtime may use any non-empty string —
34
+ * see {@link ProviderId}.
35
+ *
36
+ * MIGRATION NOTE: The legacy `ModelTransport` ('anthropic'|'openai'|'gemini'|'moonshot')
37
+ * is re-typed as `Extract<ProviderId, BuiltinProviderId>` for one release cycle
38
+ * (deprecated, then removed).
39
+ */
40
+ export type BuiltinProviderId =
41
+ | 'anthropic'
42
+ | 'openai'
43
+ | 'gemini'
44
+ | 'moonshot'
45
+ | 'openrouter'
46
+ | 'bedrock'
47
+ | 'deepseek'
48
+ | 'xai'
49
+ | 'groq'
50
+ | 'kimi-code';
51
+
52
+ /**
53
+ * Provider identity string.
54
+ *
55
+ * Builtin providers use the fixed literals in {@link BuiltinProviderId}.
56
+ * Plugin providers registered at runtime via `ProviderRegistry` may use any
57
+ * non-empty string. The open string union allows plugins without forcing
58
+ * exhaustive switch checks in transport code (match arms should include a
59
+ * default/unknown branch).
60
+ *
61
+ * @see ADR-072 §Type lock-in
62
+ */
63
+ export type ProviderId = BuiltinProviderId | (string & Record<never, never>);
@@ -0,0 +1,191 @@
1
+ /**
2
+ * ProviderProfile interface and plugin contracts for the CLEO provider registry.
3
+ *
4
+ * Ported idiomatically from the Hermes provider registry (Python dataclass +
5
+ * importlib pattern → TypeScript interface + dynamic ESM import). This is the
6
+ * canonical type lock for Phase 3 of T-LLM-CRED-CENTRALIZATION — all
7
+ * subsequent Phase 3 tasks depend on this interface shape.
8
+ *
9
+ * @task T9262
10
+ * @epic T9261 (T-LLM-CRED-CENTRALIZATION Phase 3)
11
+ */
12
+
13
+ import type { ModelTransport, StoredAuthTypeWire } from '../operations/llm.js';
14
+ import type { TransportMessage, TransportTool } from './normalized-response.js';
15
+
16
+ /**
17
+ * Describes a single LLM provider — its auth capabilities, base URL,
18
+ * default model, and optional live model-discovery hook.
19
+ *
20
+ * Builtin providers (anthropic, openai, gemini, moonshot) ship with the
21
+ * registry. User plugins under `${CLEO_HOME}/plugins/model-providers/` may
22
+ * register additional profiles or override builtins (last-writer-wins).
23
+ */
24
+ export interface ProviderProfile {
25
+ /**
26
+ * Canonical provider name. For builtin providers this MUST match a
27
+ * {@link ModelTransport} so the credential resolver can wire the two
28
+ * systems together. User plugins may use any unique string.
29
+ */
30
+ name: ModelTransport | string;
31
+
32
+ /** Human-readable display name shown in pickers and diagnostic output. */
33
+ displayName: string;
34
+
35
+ /** Authentication schemes this provider supports. */
36
+ authTypes: ReadonlyArray<StoredAuthTypeWire>;
37
+
38
+ /**
39
+ * Base URL for the provider's API — no trailing slash.
40
+ *
41
+ * @example 'https://api.anthropic.com'
42
+ */
43
+ baseUrl: string;
44
+
45
+ /**
46
+ * Recommended default model identifier for this provider.
47
+ *
48
+ * @example 'claude-haiku-X-Y' (use the latest haiku from @cleocode/core/llm)
49
+ */
50
+ defaultModel: string;
51
+
52
+ /**
53
+ * Alternate names that resolve to this profile. Resolution is
54
+ * case-insensitive. Alias conflicts with another profile's primary name
55
+ * cause a `TypeError` at registration time.
56
+ */
57
+ aliases?: ReadonlyArray<string>;
58
+
59
+ /**
60
+ * HTTP headers sent with every request to this provider.
61
+ * Merged into the SDK client's `defaultHeaders`.
62
+ *
63
+ * @example { 'anthropic-version': '2023-06-01' }
64
+ */
65
+ defaultHeaders?: Readonly<Record<string, string>>;
66
+
67
+ /**
68
+ * Environment variable names that supply credentials for this provider.
69
+ * Sourced from the `env` field in the models.dev catalog when generated,
70
+ * or declared by hand-written builtins and plugins. Resolver chains may
71
+ * check these env vars for an API key before falling back to the
72
+ * credentials store.
73
+ *
74
+ * @example ['ANTHROPIC_API_KEY']
75
+ */
76
+ envVars?: ReadonlyArray<string>;
77
+
78
+ /**
79
+ * Optional live model-discovery hook. When present, the registry may
80
+ * call this to enumerate available model identifiers. Returns `null`
81
+ * when the provider does not support live model listing or when the
82
+ * fetch fails — callers MUST fall back to static model lists in that
83
+ * case.
84
+ *
85
+ * @param apiKey - The resolved API key (or OAuth bearer token).
86
+ * @param signal - Optional abort signal for request cancellation.
87
+ */
88
+ fetchModels?: (apiKey: string, signal?: AbortSignal) => Promise<ReadonlyArray<string> | null>;
89
+
90
+ /**
91
+ * Hook: transform messages before they are passed to the SDK.
92
+ *
93
+ * Default behavior (when `undefined`): identity — messages are forwarded
94
+ * unchanged. Providers use this hook for shape conversions such as:
95
+ * - Gemini: hoisting the system message to `systemInstruction`.
96
+ * - Anthropic: injecting an assistant-prefill block for structured output.
97
+ * - OpenAI o-series: merging consecutive same-role messages.
98
+ *
99
+ * Port of Hermes `ProviderProfile.prepare_messages` (Hermes §3.1).
100
+ *
101
+ * @param messages - The transport-level messages prior to provider conversion.
102
+ * @param model - The resolved model identifier.
103
+ * @returns Possibly-modified messages array. Implementations MUST NOT mutate
104
+ * the input array; return a new array if changes are needed.
105
+ */
106
+ readonly prepareMessages?: (
107
+ messages: readonly TransportMessage[],
108
+ model: string,
109
+ ) => readonly TransportMessage[];
110
+
111
+ /**
112
+ * Hook: contribute provider-specific extra body fields.
113
+ *
114
+ * The returned object is merged into the SDK request's `extra_body`
115
+ * (OpenAI-style) or equivalent provider field. Providers use this for:
116
+ * - Gemini: `thinkingConfig` for extended reasoning budget.
117
+ * - OpenRouter: Pareto router plugin parameters.
118
+ * - Anthropic-via-OpenRouter: fine-grained tool streaming control.
119
+ *
120
+ * Port of Hermes `ProviderProfile.build_extra_body`.
121
+ *
122
+ * @param model - The resolved model identifier.
123
+ * @param messages - The transport-level messages at call time.
124
+ * @param tools - The tool definitions at call time.
125
+ * @returns Object merged into the provider request's `extra_body` field.
126
+ */
127
+ readonly buildExtraBody?: (
128
+ model: string,
129
+ messages: readonly TransportMessage[],
130
+ tools: readonly TransportTool[],
131
+ ) => Readonly<Record<string, unknown>>;
132
+
133
+ /**
134
+ * Hook: contribute provider-specific top-level API kwargs.
135
+ *
136
+ * The returned object is shallow-merged into the SDK call kwargs. Providers
137
+ * use this for top-level fields that do not fit into `extra_body`:
138
+ * - xAI Grok: `x-grok-conv-id` conversation pinning header.
139
+ * - Kimi: `reasoning_effort` top-level reasoning control.
140
+ * - Moonshot: JSON-schema sanitization applied at the request level.
141
+ *
142
+ * Port of Hermes `ProviderProfile.build_api_kwargs_extras`.
143
+ *
144
+ * @param model - The resolved model identifier.
145
+ * @param messages - The transport-level messages at call time.
146
+ * @param tools - The tool definitions at call time.
147
+ * @returns Object shallow-merged into the SDK call kwargs.
148
+ */
149
+ readonly buildApiKwargsExtras?: (
150
+ model: string,
151
+ messages: readonly TransportMessage[],
152
+ tools: readonly TransportTool[],
153
+ ) => Readonly<Record<string, unknown>>;
154
+ }
155
+
156
+ /**
157
+ * Shape that user plugin modules MUST satisfy.
158
+ *
159
+ * A plugin file is any `*.{ts,mjs,js,cjs}` under
160
+ * `${CLEO_HOME}/plugins/model-providers/`. The registry imports it
161
+ * dynamically and calls either its default export or named `register`
162
+ * export with a {@link ProviderPluginApi} instance.
163
+ *
164
+ * @example
165
+ * ```ts
166
+ * // my-plugin.mjs
167
+ * export function register(api) {
168
+ * api.registerProvider({
169
+ * name: 'my-provider',
170
+ * displayName: 'My Provider',
171
+ * authTypes: ['api_key'],
172
+ * baseUrl: 'https://api.myprovider.com',
173
+ * defaultModel: 'my-model-v1',
174
+ * });
175
+ * }
176
+ * ```
177
+ */
178
+ export interface ProviderPlugin {
179
+ /** Called by the registry with access to {@link ProviderPluginApi}. */
180
+ register: (api: ProviderPluginApi) => void;
181
+ }
182
+
183
+ /**
184
+ * Minimal surface exposed to plugin modules at load time.
185
+ * Intentionally narrow — plugins should only register providers,
186
+ * not reach into registry internals.
187
+ */
188
+ export interface ProviderPluginApi {
189
+ /** Register a provider profile. Last-writer-wins on name collision. */
190
+ registerProvider: (profile: ProviderProfile) => void;
191
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Fully-resolved credential type for the unified LLM provider architecture.
3
+ *
4
+ * This type replaces the partial `CredentialResultWire` at the runtime level.
5
+ * `CredentialResultWire` is retained solely as a wire diagnostic type for
6
+ * `cleo llm whoami` CLI output.
7
+ *
8
+ * @module llm/resolved-credential
9
+ * @task T9281
10
+ * @epic T9261 T-LLM-CRED-CENTRALIZATION
11
+ * @see ADR-072 §Type lock-in
12
+ */
13
+
14
+ import type { ProviderId } from './provider-id.js';
15
+
16
+ /**
17
+ * Fully-resolved credential ready for use in a transport constructor.
18
+ *
19
+ * The transport consumes this at construction time and MUST NOT store it
20
+ * beyond initialization. {@link extraHeaders} is merged into SDK
21
+ * defaultHeaders.
22
+ *
23
+ * Replaces the partial `CredentialResultWire` for runtime use.
24
+ * `CredentialResultWire` is retained as a wire diagnostic type for
25
+ * `cleo llm whoami` CLI output only.
26
+ *
27
+ * @see ADR-072 §Type lock-in
28
+ */
29
+ export interface ResolvedCredential {
30
+ /** Provider this credential targets. */
31
+ readonly provider: ProviderId;
32
+ /** Human-readable store label (e.g. 'personal', 'work'). */
33
+ readonly label: string;
34
+ /**
35
+ * API key, OAuth bearer token, or empty string for aws_sdk auth.
36
+ * NEVER log this field. NEVER include in NormalizedResponse.providerData.
37
+ */
38
+ readonly token: string;
39
+ /** How the token is presented to the provider. */
40
+ readonly authType: 'api_key' | 'oauth' | 'aws_sdk';
41
+ /**
42
+ * Unix epoch ms at which the token expires. null = never expires.
43
+ * LlmSession checks this before each send() and triggers OAuth refresh
44
+ * when less than 60 seconds remain.
45
+ */
46
+ readonly expiresAt: number | null;
47
+ /**
48
+ * OAuth refresh token. null for api_key and aws_sdk credentials.
49
+ * LlmSession holds this; the transport never sees it.
50
+ */
51
+ readonly refreshToken: string | null;
52
+ /**
53
+ * Extra HTTP headers merged into every SDK request from this credential.
54
+ * Typically carries 'Authorization: Bearer ...' for oauth authType,
55
+ * and 'anthropic-beta: ...' when needed.
56
+ */
57
+ readonly extraHeaders: Readonly<Record<string, string>>;
58
+ /** Base URL override (proxy, on-prem deployment). null = use provider default. */
59
+ readonly baseUrl: string | null;
60
+ /** AWS profile name for Bedrock. null for all other providers. */
61
+ readonly awsProfile: string | null;
62
+ }
@@ -9,8 +9,21 @@
9
9
  * @epic T1386
10
10
  */
11
11
 
12
- /** Supported provider transport names. */
13
- export type ModelTransport = 'anthropic' | 'openai' | 'gemini' | 'moonshot';
12
+ import type { BuiltinProviderId, ProviderId } from '../llm/provider-id.js';
13
+
14
+ /**
15
+ * @deprecated since Phase 4 (ADR-072). Use {@link ProviderId} from
16
+ * `@cleocode/contracts/llm/provider-id.js` for new code. This alias remains
17
+ * for one release cycle to ease migration of legacy callers, then is removed.
18
+ *
19
+ * The alias resolves to {@link BuiltinProviderId} — a superset of the previous
20
+ * closed literal union (`'anthropic' | 'openai' | 'gemini' | 'moonshot'`).
21
+ * All existing consumers that pattern-match on the previous four values continue
22
+ * to compile without changes.
23
+ *
24
+ * @see ADR-072 §Type lock-in — ModelTransport deprecation cycle
25
+ */
26
+ export type ModelTransport = Extract<ProviderId, BuiltinProviderId>;
14
27
 
15
28
  /** Cache policy mode for prompt prefix caching. */
16
29
  export type PromptCachePolicyMode = 'gemini_cached_content';
@@ -550,3 +563,8 @@ export interface LlmWhoamiResult {
550
563
  /** One entry per role resolved (filtered by `params.role` when set). */
551
564
  entries: LlmWhoamiEntry[];
552
565
  }
566
+
567
+ // ProviderProfile interface lives in ./llm/provider-profile.ts (T9262).
568
+ // The generated catalog (packages/core/src/llm/generated/provider-profiles.ts)
569
+ // imports the canonical type from the contracts package and emits each
570
+ // generated entry as a plain ProviderProfile literal.