@hasna/knowledge 0.2.10 → 0.2.11

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.
package/src/cli.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  import { defaultStorePath, loadStore, saveStore, withLock, makeId, makeShortId, ensureStore, type KnowledgeItem } from './store';
8
8
  import { openKnowledgeDb } from './knowledge-db';
9
9
  import { createKnowledgeService } from './service';
10
+ import { assertProviderCredentials, parseModelRef, resolveModelRef, type AiProviderId } from './providers';
10
11
  import { approvalStatus, assertS3ReadAllowed, assertWebSearchAllowed, createApprovalGate, recordAuditEvent, recordRedactionFindings, redactSecrets } from './safety';
11
12
  import pkg from '../package.json' with { type: 'json' };
12
13
 
@@ -59,7 +60,7 @@ interface ParseResult {
59
60
  flags: Flags;
60
61
  }
61
62
 
62
- const COMMANDS = ['add', 'list', 'get', 'delete', 'update', 'archive', 'restore', 'upsert', 'untag', 'export', 'prune', 'dedupe', 'stats', 'paths', 'db', 'wiki', 'source', 'ingest', 'reindex', 'safety', 'help'];
63
+ const COMMANDS = ['add', 'list', 'get', 'delete', 'update', 'archive', 'restore', 'upsert', 'untag', 'export', 'prune', 'dedupe', 'stats', 'paths', 'db', 'wiki', 'source', 'ingest', 'reindex', 'providers', 'safety', 'help'];
63
64
  const COMMAND_ALIASES: Record<string, string> = {
64
65
  ls: 'list',
65
66
  rm: 'delete',
@@ -167,6 +168,7 @@ Commands:
167
168
  ingest manifest <file|s3://> Ingest an open-files manifest into knowledge.db
168
169
  ingest source <source-ref> Ingest a read-only source ref into knowledge.db
169
170
  reindex outbox <file|s3://> Consume open-files change events and invalidate chunks
171
+ providers status|models|check Inspect AI SDK provider config and credentials
170
172
  safety status|check|approve|audit|redact
171
173
  help [command] Show help
172
174
 
@@ -233,6 +235,7 @@ function printCommandHelp(command: string): void {
233
235
  if (command === 'source') { console.log('Usage: open-knowledge source resolve <source-ref> [--purpose knowledge_answer|knowledge_index] [--limit <n>] [--scope local|global|project] [--json]'); return; }
234
236
  if (command === 'ingest') { console.log('Usage: open-knowledge ingest manifest <file|s3://bucket/key> | source <source-ref> [--purpose knowledge_index] [--scope local|global|project] [--json]'); return; }
235
237
  if (command === 'reindex') { console.log('Usage: open-knowledge reindex outbox <file|s3://bucket/key> [--scope local|global|project] [--json]'); return; }
238
+ if (command === 'providers') { console.log('Usage: open-knowledge providers status|models|check [provider|model-alias] [--scope local|global|project] [--json]'); return; }
236
239
  if (command === 'safety') { console.log('Usage: open-knowledge safety status|check|approve|audit|redact [args] [--scope local|global|project] [--json]'); return; }
237
240
  printGlobalHelp();
238
241
  }
@@ -278,11 +281,11 @@ async function run(argv: string[]): Promise<void> {
278
281
  if (flags.completions) {
279
282
  const shell = flags.completions;
280
283
  if (shell === 'bash') {
281
- console.log(`_open_knowledge() { local cur; cur="${"$"}{COMP_WORDS[COMP_CWORD]}"; COMPREPLY=($(compgen -W "add list get update archive restore upsert untag delete export prune dedupe stats paths db wiki source ingest reindex safety help ls rm edit unarchive --json --yes --help --version --desc --page --limit --search --sort --id --store --title --content --url --tag --format --completions --purpose --no-color --scope --archived --include-archived" -- "$cur")); }; complete -F _open_knowledge open-knowledge`);
284
+ console.log(`_open_knowledge() { local cur; cur="${"$"}{COMP_WORDS[COMP_CWORD]}"; COMPREPLY=($(compgen -W "add list get update archive restore upsert untag delete export prune dedupe stats paths db wiki source ingest reindex providers safety help ls rm edit unarchive --json --yes --help --version --desc --page --limit --search --sort --id --store --title --content --url --tag --format --completions --purpose --no-color --scope --archived --include-archived" -- "$cur")); }; complete -F _open_knowledge open-knowledge`);
282
285
  } else if (shell === 'zsh') {
283
- console.log(`#compdef open-knowledge\n_open_knowledge() { _arguments -C "1: :(add list get update archive restore upsert untag delete export prune dedupe stats paths db wiki source ingest reindex safety help ls rm edit unarchive)" "(--json)--json" "(--yes)-y" "(--help)--help" "(--version)--version" "(--desc)--desc" "(--archived)--archived" "(--include-archived)--include-archived" "(-p --page)"{-p,--page}"[page number]:number:" "(-l --limit)"{-l,--limit}"[items per page]:number:" "(-s --search)"{-s,--search}"[search text]:text:" "(--sort)--sort"\{created,title\}:" "(--id)--id[item id]:id:" "(--store)--store[store path]:path:" "(--title)--title[new title]:" "(--content)--content[new content]:" "(--url)--url[source url]:" "(-t --tag)"{-t,--tag}"[tag]:tag:" "(--format)--format[json|jsonl]:" "(--completions)--completions[output completions]:shell:(bash zsh fish):" "(--purpose)--purpose[purpose]:" "(--no-color)--no-color[disable color]" "(--scope)--scope"\{local,global,project\}:" }; _open_knowledge`);
286
+ console.log(`#compdef open-knowledge\n_open_knowledge() { _arguments -C "1: :(add list get update archive restore upsert untag delete export prune dedupe stats paths db wiki source ingest reindex providers safety help ls rm edit unarchive)" "(--json)--json" "(--yes)-y" "(--help)--help" "(--version)--version" "(--desc)--desc" "(--archived)--archived" "(--include-archived)--include-archived" "(-p --page)"{-p,--page}"[page number]:number:" "(-l --limit)"{-l,--limit}"[items per page]:number:" "(-s --search)"{-s,--search}"[search text]:text:" "(--sort)--sort"\{created,title\}:" "(--id)--id[item id]:id:" "(--store)--store[store path]:path:" "(--title)--title[new title]:" "(--content)--content[new content]:" "(--url)--url[source url]:" "(-t --tag)"{-t,--tag}"[tag]:tag:" "(--format)--format[json|jsonl]:" "(--completions)--completions[output completions]:shell:(bash zsh fish):" "(--purpose)--purpose[purpose]:" "(--no-color)--no-color[disable color]" "(--scope)--scope"\{local,global,project\}:" }; _open_knowledge`);
284
287
  } else if (shell === 'fish') {
285
- console.log(`complete -c open-knowledge -f; complete -c open-knowledge -a "add list get update archive restore upsert untag delete export prune dedupe stats paths db wiki source ingest reindex safety help ls rm edit unarchive"; complete -c open-knowledge -l json; complete -c open-knowledge -l yes -s y; complete -c open-knowledge -l help -s h; complete -c open-knowledge -l version -s v; complete -c open-knowledge -l desc; complete -c open-knowledge -l archived; complete -c open-knowledge -l include-archived; complete -c open-knowledge -s p -l page; complete -c open-knowledge -s l -l limit; complete -c open-knowledge -s s -l search; complete -c open-knowledge -l sort; complete -c open-knowledge -l id; complete -c open-knowledge -l store; complete -c open-knowledge -l title; complete -c open-knowledge -l content; complete -c open-knowledge -l url; complete -c open-knowledge -s t -l tag; complete -c open-knowledge -l format; complete -c open-knowledge -l completions; complete -c open-knowledge -l purpose; complete -c open-knowledge -l no-color; complete -c open-knowledge -l scope -a "local global project"`);
288
+ console.log(`complete -c open-knowledge -f; complete -c open-knowledge -a "add list get update archive restore upsert untag delete export prune dedupe stats paths db wiki source ingest reindex providers safety help ls rm edit unarchive"; complete -c open-knowledge -l json; complete -c open-knowledge -l yes -s y; complete -c open-knowledge -l help -s h; complete -c open-knowledge -l version -s v; complete -c open-knowledge -l desc; complete -c open-knowledge -l archived; complete -c open-knowledge -l include-archived; complete -c open-knowledge -s p -l page; complete -c open-knowledge -s l -l limit; complete -c open-knowledge -s s -l search; complete -c open-knowledge -l sort; complete -c open-knowledge -l id; complete -c open-knowledge -l store; complete -c open-knowledge -l title; complete -c open-knowledge -l content; complete -c open-knowledge -l url; complete -c open-knowledge -s t -l tag; complete -c open-knowledge -l format; complete -c open-knowledge -l completions; complete -c open-knowledge -l purpose; complete -c open-knowledge -l no-color; complete -c open-knowledge -l scope -a "local global project"`);
286
289
  } else {
287
290
  throw new Error("Invalid --completions value. Use 'bash', 'zsh', or 'fish'.");
288
291
  }
@@ -504,6 +507,30 @@ async function run(argv: string[]): Promise<void> {
504
507
  return;
505
508
  }
506
509
 
510
+ if (command === 'providers') {
511
+ const action = positional[1] ?? 'status';
512
+ if (action === 'status') {
513
+ const status = service.providerStatus();
514
+ const configured = status.providers.filter((entry) => entry.configured).length;
515
+ output({ ok: true, ...status, message: `${configured}/${status.providers.length} provider credential(s) configured` }, flags.json);
516
+ return;
517
+ }
518
+ if (action === 'models') {
519
+ const models = service.modelRegistry();
520
+ output({ ok: true, models, message: `${models.length} model alias(es)` }, flags.json);
521
+ return;
522
+ }
523
+ if (action === 'check') {
524
+ const target = positional[2] ?? 'default';
525
+ const modelRef = resolveModelRef(target, service.config());
526
+ const parsed = parseModelRef(modelRef);
527
+ const credential = assertProviderCredentials(parsed.provider as AiProviderId, service.config());
528
+ output({ ok: true, target, model_ref: modelRef, provider: parsed.provider, model: parsed.model, credential, message: `${parsed.provider} credentials configured` }, flags.json);
529
+ return;
530
+ }
531
+ throw new Error("Invalid providers action. Use 'status', 'models', or 'check'.");
532
+ }
533
+
507
534
  ensureStore(storePath);
508
535
 
509
536
  if (command === 'add') {
package/src/mcp.js CHANGED
@@ -105,6 +105,20 @@ export function buildServer() {
105
105
  }
106
106
  });
107
107
 
108
+ registerTool(server, 'ok_provider_status', 'AI provider status', 'Inspect configured AI SDK providers, model aliases, and BYOK credential availability', {
109
+ scope: scopeField,
110
+ }, async ({ scope }) => {
111
+ const service = createKnowledgeService({ scope });
112
+ return jsonText({ ok: true, ...service.providerStatus() });
113
+ });
114
+
115
+ registerTool(server, 'ok_provider_models', 'AI provider models', 'List AI SDK model aliases and capability metadata', {
116
+ scope: scopeField,
117
+ }, async ({ scope }) => {
118
+ const service = createKnowledgeService({ scope });
119
+ return jsonText({ ok: true, models: service.modelRegistry() });
120
+ });
121
+
108
122
  registerTool(server, 'ok_add', 'Add a knowledge item', 'Add a new item to the knowledge store', {
109
123
  title: z.string().describe('Item title'),
110
124
  content: z.string().describe('Item content/body'),
@@ -0,0 +1,308 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import type { Database } from 'bun:sqlite';
3
+ import type { KnowledgeConfig } from './workspace';
4
+
5
+ export type AiProviderId = 'openai' | 'anthropic' | 'deepseek';
6
+
7
+ export interface AiProviderSettings {
8
+ api_key_env: string;
9
+ base_url?: string;
10
+ default_model: string;
11
+ }
12
+
13
+ export interface AiProvidersConfig {
14
+ default_model?: string;
15
+ aliases?: Record<string, string>;
16
+ openai?: Partial<AiProviderSettings>;
17
+ anthropic?: Partial<AiProviderSettings>;
18
+ deepseek?: Partial<AiProviderSettings>;
19
+ }
20
+
21
+ export interface ModelCapabilities {
22
+ text_generation: boolean;
23
+ structured_output: boolean;
24
+ tool_usage: boolean;
25
+ tool_streaming: boolean;
26
+ image_input: boolean;
27
+ native_web_search: boolean;
28
+ reasoning: boolean;
29
+ embeddings: boolean;
30
+ }
31
+
32
+ export interface ModelRegistryEntry {
33
+ alias: string;
34
+ model_ref: string;
35
+ provider: AiProviderId;
36
+ model: string;
37
+ default: boolean;
38
+ capabilities: ModelCapabilities;
39
+ }
40
+
41
+ export interface ProviderCredentialStatus {
42
+ provider: AiProviderId;
43
+ api_key_env: string;
44
+ configured: boolean;
45
+ source: 'env' | 'missing';
46
+ base_url: string | null;
47
+ default_model: string;
48
+ }
49
+
50
+ export interface ProviderStatusResult {
51
+ default_model: string;
52
+ providers: ProviderCredentialStatus[];
53
+ models: ModelRegistryEntry[];
54
+ }
55
+
56
+ export interface NormalizedProviderUsage {
57
+ provider: string;
58
+ model: string;
59
+ input_tokens: number;
60
+ output_tokens: number;
61
+ cost_usd: number;
62
+ metadata: Record<string, unknown>;
63
+ }
64
+
65
+ type ProviderFactory = (settings: { apiKey: string; baseURL?: string }) => unknown;
66
+
67
+ export interface AiProviderRuntimeOptions {
68
+ config?: KnowledgeConfig;
69
+ env?: Record<string, string | undefined>;
70
+ factories?: Partial<Record<AiProviderId, ProviderFactory>>;
71
+ }
72
+
73
+ const DEFAULT_PROVIDER_SETTINGS: Record<AiProviderId, AiProviderSettings> = {
74
+ openai: {
75
+ api_key_env: 'OPENAI_API_KEY',
76
+ default_model: 'gpt-5.2',
77
+ },
78
+ anthropic: {
79
+ api_key_env: 'ANTHROPIC_API_KEY',
80
+ default_model: 'claude-sonnet-4-6',
81
+ },
82
+ deepseek: {
83
+ api_key_env: 'DEEPSEEK_API_KEY',
84
+ default_model: 'deepseek-chat',
85
+ },
86
+ };
87
+
88
+ const PROVIDER_CAPABILITIES: Record<AiProviderId, ModelCapabilities> = {
89
+ openai: {
90
+ text_generation: true,
91
+ structured_output: true,
92
+ tool_usage: true,
93
+ tool_streaming: true,
94
+ image_input: true,
95
+ native_web_search: true,
96
+ reasoning: true,
97
+ embeddings: true,
98
+ },
99
+ anthropic: {
100
+ text_generation: true,
101
+ structured_output: true,
102
+ tool_usage: true,
103
+ tool_streaming: true,
104
+ image_input: true,
105
+ native_web_search: false,
106
+ reasoning: true,
107
+ embeddings: false,
108
+ },
109
+ deepseek: {
110
+ text_generation: true,
111
+ structured_output: true,
112
+ tool_usage: true,
113
+ tool_streaming: true,
114
+ image_input: false,
115
+ native_web_search: false,
116
+ reasoning: true,
117
+ embeddings: false,
118
+ },
119
+ };
120
+
121
+ const BUILTIN_ALIASES: Record<string, string> = {
122
+ default: 'openai:gpt-5.2',
123
+ fast: 'openai:gpt-5-mini',
124
+ reasoning: 'anthropic:claude-opus-4-6',
125
+ sonnet: 'anthropic:claude-sonnet-4-6',
126
+ deepseek: 'deepseek:deepseek-chat',
127
+ 'deepseek-reasoning': 'deepseek:deepseek-reasoner',
128
+ };
129
+
130
+ function providerConfig(config?: KnowledgeConfig): AiProvidersConfig {
131
+ return (config as KnowledgeConfig & { providers?: AiProvidersConfig }).providers ?? {};
132
+ }
133
+
134
+ export function providerSettings(config: KnowledgeConfig | undefined, provider: AiProviderId): AiProviderSettings {
135
+ const configured = providerConfig(config)[provider] ?? {};
136
+ return {
137
+ ...DEFAULT_PROVIDER_SETTINGS[provider],
138
+ ...configured,
139
+ };
140
+ }
141
+
142
+ export function modelAliases(config?: KnowledgeConfig): Record<string, string> {
143
+ const configured = providerConfig(config);
144
+ return {
145
+ ...BUILTIN_ALIASES,
146
+ ...(configured.default_model ? { default: configured.default_model } : {}),
147
+ ...(configured.aliases ?? {}),
148
+ };
149
+ }
150
+
151
+ export function parseModelRef(modelRef: string): { provider: AiProviderId; model: string } {
152
+ const [provider, ...rest] = modelRef.split(':');
153
+ const model = rest.join(':');
154
+ if (provider !== 'openai' && provider !== 'anthropic' && provider !== 'deepseek') {
155
+ throw new Error(`Unsupported AI provider: ${provider}`);
156
+ }
157
+ if (!model) throw new Error(`Invalid model ref: ${modelRef}. Expected provider:model.`);
158
+ return { provider, model };
159
+ }
160
+
161
+ export function resolveModelRef(aliasOrRef: string, config?: KnowledgeConfig): string {
162
+ const aliases = modelAliases(config);
163
+ return aliases[aliasOrRef] ?? aliasOrRef;
164
+ }
165
+
166
+ export function listModelRegistry(config?: KnowledgeConfig): ModelRegistryEntry[] {
167
+ const aliases = modelAliases(config);
168
+ return Object.entries(aliases).map(([alias, modelRef]) => {
169
+ const parsed = parseModelRef(modelRef);
170
+ return {
171
+ alias,
172
+ model_ref: modelRef,
173
+ provider: parsed.provider,
174
+ model: parsed.model,
175
+ default: alias === 'default',
176
+ capabilities: PROVIDER_CAPABILITIES[parsed.provider],
177
+ };
178
+ });
179
+ }
180
+
181
+ export function providerCredentialStatus(config: KnowledgeConfig | undefined, env: Record<string, string | undefined> = process.env): ProviderCredentialStatus[] {
182
+ return (Object.keys(DEFAULT_PROVIDER_SETTINGS) as AiProviderId[]).map((provider) => {
183
+ const settings = providerSettings(config, provider);
184
+ const configured = Boolean(env[settings.api_key_env]);
185
+ return {
186
+ provider,
187
+ api_key_env: settings.api_key_env,
188
+ configured,
189
+ source: configured ? 'env' : 'missing',
190
+ base_url: settings.base_url ?? null,
191
+ default_model: settings.default_model,
192
+ };
193
+ });
194
+ }
195
+
196
+ export function providerStatus(config?: KnowledgeConfig, env: Record<string, string | undefined> = process.env): ProviderStatusResult {
197
+ return {
198
+ default_model: resolveModelRef('default', config),
199
+ providers: providerCredentialStatus(config, env),
200
+ models: listModelRegistry(config),
201
+ };
202
+ }
203
+
204
+ export function assertProviderCredentials(provider: AiProviderId, config?: KnowledgeConfig, env: Record<string, string | undefined> = process.env): ProviderCredentialStatus {
205
+ const status = providerCredentialStatus(config, env).find((entry) => entry.provider === provider);
206
+ if (!status) throw new Error(`Unsupported AI provider: ${provider}`);
207
+ if (!status.configured) throw new Error(`Missing ${status.api_key_env} for ${provider}. Set the env var to use this provider.`);
208
+ return status;
209
+ }
210
+
211
+ async function defaultFactory(provider: AiProviderId): Promise<ProviderFactory> {
212
+ if (provider === 'openai') {
213
+ const { createOpenAI } = await import('@ai-sdk/openai');
214
+ return createOpenAI as ProviderFactory;
215
+ }
216
+ if (provider === 'anthropic') {
217
+ const { createAnthropic } = await import('@ai-sdk/anthropic');
218
+ return createAnthropic as ProviderFactory;
219
+ }
220
+ const { createDeepSeek } = await import('@ai-sdk/deepseek');
221
+ return createDeepSeek as ProviderFactory;
222
+ }
223
+
224
+ export async function createAiSdkProviderRegistry(options: AiProviderRuntimeOptions = {}) {
225
+ const { createProviderRegistry } = await import('ai');
226
+ const env = options.env ?? process.env;
227
+ const providers: Record<string, unknown> = {};
228
+ for (const provider of Object.keys(DEFAULT_PROVIDER_SETTINGS) as AiProviderId[]) {
229
+ const settings = providerSettings(options.config, provider);
230
+ const apiKey = env[settings.api_key_env];
231
+ if (!apiKey) continue;
232
+ const factory = options.factories?.[provider] ?? await defaultFactory(provider);
233
+ providers[provider] = factory({ apiKey, baseURL: settings.base_url });
234
+ }
235
+ return createProviderRegistry(providers as never);
236
+ }
237
+
238
+ export async function languageModelFor(aliasOrRef: string, options: AiProviderRuntimeOptions = {}) {
239
+ const modelRef = resolveModelRef(aliasOrRef, options.config);
240
+ const parsed = parseModelRef(modelRef);
241
+ assertProviderCredentials(parsed.provider, options.config, options.env);
242
+ const registry = await createAiSdkProviderRegistry(options);
243
+ return registry.languageModel(modelRef as `${string}:${string}`);
244
+ }
245
+
246
+ function usageNumber(usage: Record<string, unknown>, keys: string[]): number {
247
+ for (const key of keys) {
248
+ const value = usage[key];
249
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
250
+ }
251
+ return 0;
252
+ }
253
+
254
+ export function normalizeAiSdkUsage(input: {
255
+ provider: string;
256
+ model: string;
257
+ usage?: Record<string, unknown> | null;
258
+ providerMetadata?: Record<string, unknown> | null;
259
+ costUsd?: number;
260
+ }): NormalizedProviderUsage {
261
+ const usage = input.usage ?? {};
262
+ return {
263
+ provider: input.provider,
264
+ model: input.model,
265
+ input_tokens: usageNumber(usage, ['inputTokens', 'promptTokens', 'input_tokens', 'prompt_tokens']),
266
+ output_tokens: usageNumber(usage, ['outputTokens', 'completionTokens', 'output_tokens', 'completion_tokens']),
267
+ cost_usd: input.costUsd ?? 0,
268
+ metadata: {
269
+ usage,
270
+ provider_metadata: input.providerMetadata ?? {},
271
+ },
272
+ };
273
+ }
274
+
275
+ export function recordProviderUsage(db: Database, input: NormalizedProviderUsage & { run_id?: string | null; created_at?: string }): string {
276
+ const id = `usage_${randomUUID()}`;
277
+ db.run(
278
+ `INSERT INTO provider_usage (id, run_id, provider, model, input_tokens, output_tokens, cost_usd, metadata_json, created_at)
279
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
280
+ [
281
+ id,
282
+ input.run_id ?? null,
283
+ input.provider,
284
+ input.model,
285
+ input.input_tokens,
286
+ input.output_tokens,
287
+ input.cost_usd,
288
+ JSON.stringify(input.metadata),
289
+ input.created_at ?? new Date().toISOString(),
290
+ ],
291
+ );
292
+ return id;
293
+ }
294
+
295
+ export function createDeterministicFakeProvider(provider: AiProviderId): ProviderFactory {
296
+ return () => ({
297
+ languageModel: (modelId: string) => ({
298
+ provider,
299
+ modelId,
300
+ specificationVersion: 'v3',
301
+ }),
302
+ chat: (modelId: string) => ({
303
+ provider,
304
+ modelId,
305
+ specificationVersion: 'v3',
306
+ }),
307
+ });
308
+ }
package/src/service.ts CHANGED
@@ -4,6 +4,7 @@ import { getKnowledgeDbStats, migrateKnowledgeDb } from './knowledge-db';
4
4
  import { ingestOpenFilesManifest } from './manifest-ingest';
5
5
  import { ingestSourceRef } from './source-ingest';
6
6
  import { resolveOpenFilesSource } from './source-resolver';
7
+ import { providerStatus, listModelRegistry, type ProviderStatusResult, type ModelRegistryEntry } from './providers';
7
8
  import { resolveSafetyPolicy } from './safety';
8
9
  import { initializeWikiLayout } from './wiki-layout';
9
10
  import {
@@ -150,6 +151,14 @@ export class KnowledgeService {
150
151
  safetyPolicy: this.safetyPolicy(),
151
152
  });
152
153
  }
154
+
155
+ providerStatus(env: Record<string, string | undefined> = process.env): ProviderStatusResult {
156
+ return providerStatus(this.config(), env);
157
+ }
158
+
159
+ modelRegistry(): ModelRegistryEntry[] {
160
+ return listModelRegistry(this.config());
161
+ }
153
162
  }
154
163
 
155
164
  export function createKnowledgeService(options: KnowledgeServiceOptions = {}): KnowledgeService {
package/src/workspace.ts CHANGED
@@ -39,6 +39,25 @@ export interface KnowledgeConfig {
39
39
  preferred_ref: 'open-files';
40
40
  allowed_schemes: string[];
41
41
  };
42
+ providers?: {
43
+ default_model?: string;
44
+ aliases?: Record<string, string>;
45
+ openai?: {
46
+ api_key_env?: string;
47
+ base_url?: string;
48
+ default_model?: string;
49
+ };
50
+ anthropic?: {
51
+ api_key_env?: string;
52
+ base_url?: string;
53
+ default_model?: string;
54
+ };
55
+ deepseek?: {
56
+ api_key_env?: string;
57
+ base_url?: string;
58
+ default_model?: string;
59
+ };
60
+ };
42
61
  safety?: {
43
62
  network?: {
44
63
  web_search_enabled?: boolean;
@@ -95,6 +114,28 @@ export function defaultKnowledgeConfig(): KnowledgeConfig {
95
114
  preferred_ref: 'open-files',
96
115
  allowed_schemes: ['open-files', 's3', 'file', 'https', 'http'],
97
116
  },
117
+ providers: {
118
+ default_model: 'openai:gpt-5.2',
119
+ aliases: {
120
+ fast: 'openai:gpt-5-mini',
121
+ reasoning: 'anthropic:claude-opus-4-6',
122
+ sonnet: 'anthropic:claude-sonnet-4-6',
123
+ deepseek: 'deepseek:deepseek-chat',
124
+ 'deepseek-reasoning': 'deepseek:deepseek-reasoner',
125
+ },
126
+ openai: {
127
+ api_key_env: 'OPENAI_API_KEY',
128
+ default_model: 'gpt-5.2',
129
+ },
130
+ anthropic: {
131
+ api_key_env: 'ANTHROPIC_API_KEY',
132
+ default_model: 'claude-sonnet-4-6',
133
+ },
134
+ deepseek: {
135
+ api_key_env: 'DEEPSEEK_API_KEY',
136
+ default_model: 'deepseek-chat',
137
+ },
138
+ },
98
139
  safety: {
99
140
  network: {
100
141
  web_search_enabled: false,