@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/README.md +22 -0
- package/bin/open-knowledge-mcp.js +169 -2
- package/bin/open-knowledge.js +32 -31
- package/docs/architecture/ai-native-knowledge-base.md +15 -0
- package/package.json +6 -2
- package/src/cli.ts +31 -4
- package/src/mcp.js +14 -0
- package/src/providers.ts +308 -0
- package/src/service.ts +9 -0
- package/src/workspace.ts +41 -0
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'),
|
package/src/providers.ts
ADDED
|
@@ -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,
|