@kal-elsam/kairo-runtime 0.1.4 → 0.2.0
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 +23 -0
- package/package.json +1 -1
- package/src/cli.js +113 -4
- package/src/global/action-planner.js +230 -0
- package/src/global/agent-capabilities/create-capability-adapter.js +224 -0
- package/src/global/agent-capabilities/index.js +54 -0
- package/src/global/brand/cli.js +11 -7
- package/src/global/capability-registry.js +80 -0
- package/src/global/capability-states.js +30 -0
- package/src/global/cli-probe.js +47 -0
- package/src/global/ink/orchestrator-app.js +224 -0
- package/src/global/ink/orchestrator-state.js +107 -0
- package/src/global/ink/run-orchestrator-ink.js +29 -0
- package/src/global/intelligence/backends/custom-http.js +175 -0
- package/src/global/intelligence/backends/ollama.js +164 -0
- package/src/global/intelligence/backends/openrouter.js +198 -0
- package/src/global/intelligence/context-compiler.js +338 -0
- package/src/global/intelligence/custom-url.js +82 -0
- package/src/global/intelligence/http.js +63 -0
- package/src/global/intelligence/index.js +38 -0
- package/src/global/intelligence/orchestrate.js +189 -0
- package/src/global/intelligence/registry.js +77 -0
- package/src/global/intelligence/router.js +191 -0
- package/src/global/intelligence/types.js +99 -0
- package/src/global/intelligence-cli.js +323 -0
- package/src/global/orchestrator.js +150 -0
- package/src/global/paths.js +1 -0
- package/src/global/profile.js +297 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BACKEND_IDS,
|
|
3
|
+
COST_CLASSES,
|
|
4
|
+
PRIVACY_CLASSES,
|
|
5
|
+
createModelDescriptor
|
|
6
|
+
} from "../types.js";
|
|
7
|
+
import { fetchJson } from "../http.js";
|
|
8
|
+
import { CAPABILITY_STATES } from "../../capability-states.js";
|
|
9
|
+
import { classifyCustomBaseUrl, isValidEnvironmentName } from "../custom-url.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* OpenAI-compatible HTTP backend. API keys are read from env by name only —
|
|
13
|
+
* never from profile JSON or disk.
|
|
14
|
+
*/
|
|
15
|
+
export function createCustomHttpBackend({
|
|
16
|
+
id = BACKEND_IDS.CUSTOM,
|
|
17
|
+
label = "Custom provider",
|
|
18
|
+
baseUrl,
|
|
19
|
+
modelId,
|
|
20
|
+
apiKeyEnv = null,
|
|
21
|
+
local: _local = false,
|
|
22
|
+
fetchImpl = globalThis.fetch,
|
|
23
|
+
env = process.env
|
|
24
|
+
} = {}) {
|
|
25
|
+
if (!baseUrl) {
|
|
26
|
+
throw new Error("Custom HTTP backend requires baseUrl.");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const location = classifyCustomBaseUrl(baseUrl);
|
|
30
|
+
const normalizedBase = location.normalizedBaseUrl;
|
|
31
|
+
if (apiKeyEnv != null && !isValidEnvironmentName(apiKeyEnv)) {
|
|
32
|
+
throw new Error("Custom provider apiKeyEnv must be a valid uppercase environment variable name.");
|
|
33
|
+
}
|
|
34
|
+
if (apiKeyEnv && !location.local) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"Remote custom providers cannot use apiKeyEnv in 0.2.0; use a built-in provider or a local endpoint."
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
const apiKey = apiKeyEnv ? (env[apiKeyEnv] ?? null) : null;
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
id,
|
|
43
|
+
label,
|
|
44
|
+
local: location.local,
|
|
45
|
+
|
|
46
|
+
async detect() {
|
|
47
|
+
if (apiKeyEnv && !apiKey) {
|
|
48
|
+
return {
|
|
49
|
+
id,
|
|
50
|
+
label,
|
|
51
|
+
state: CAPABILITY_STATES.UNKNOWN,
|
|
52
|
+
detected: false,
|
|
53
|
+
available: false,
|
|
54
|
+
hasApiKey: false,
|
|
55
|
+
error: null,
|
|
56
|
+
recommendation: `Set ${apiKeyEnv} in the environment. Kairo never stores credentials.`
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
id,
|
|
62
|
+
label,
|
|
63
|
+
state: CAPABILITY_STATES.DETECTED,
|
|
64
|
+
detected: true,
|
|
65
|
+
available: Boolean(modelId),
|
|
66
|
+
hasApiKey: apiKeyEnv ? Boolean(apiKey) : null,
|
|
67
|
+
error: null,
|
|
68
|
+
recommendation: modelId
|
|
69
|
+
? `Custom provider configured (${modelId}).`
|
|
70
|
+
: "Custom provider baseUrl set; configure modelId in profile."
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
async listModels() {
|
|
75
|
+
if (!modelId) return [];
|
|
76
|
+
return [
|
|
77
|
+
createModelDescriptor({
|
|
78
|
+
provider: id,
|
|
79
|
+
modelId,
|
|
80
|
+
local: location.local,
|
|
81
|
+
costClass: location.local ? COST_CLASSES.LOCAL : COST_CLASSES.UNKNOWN,
|
|
82
|
+
privacyClass: location.local ? PRIVACY_CLASSES.LOCAL : PRIVACY_CLASSES.CLOUD,
|
|
83
|
+
opaque: true
|
|
84
|
+
})
|
|
85
|
+
];
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
async capabilities() {
|
|
89
|
+
const detection = await this.detect();
|
|
90
|
+
return {
|
|
91
|
+
id,
|
|
92
|
+
local: location.local,
|
|
93
|
+
cloud: !location.local,
|
|
94
|
+
requiresApiKey: Boolean(apiKeyEnv),
|
|
95
|
+
requiresConsent: !location.local,
|
|
96
|
+
streaming: false,
|
|
97
|
+
tools: false,
|
|
98
|
+
state: detection.state,
|
|
99
|
+
baseUrl: normalizedBase,
|
|
100
|
+
modelId
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
async invoke(contextPack, request = {}) {
|
|
105
|
+
if (apiKeyEnv && !apiKey) {
|
|
106
|
+
return invokeError(id, `Missing ${apiKeyEnv}. Kairo never stores credentials.`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const resolvedModel = request.modelId ?? modelId;
|
|
110
|
+
if (!resolvedModel) {
|
|
111
|
+
return invokeError(id, "Custom invoke requires modelId.");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const messages = [];
|
|
115
|
+
if (contextPack?.systemPrompt) {
|
|
116
|
+
messages.push({ role: "system", content: contextPack.systemPrompt });
|
|
117
|
+
}
|
|
118
|
+
if (Array.isArray(request.messages) && request.messages.length > 0) {
|
|
119
|
+
messages.push(...request.messages);
|
|
120
|
+
} else if (request.prompt) {
|
|
121
|
+
messages.push({ role: "user", content: request.prompt });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const headers = { "Content-Type": "application/json" };
|
|
125
|
+
if (apiKey && location.credentialSafe) headers.Authorization = `Bearer ${apiKey}`;
|
|
126
|
+
|
|
127
|
+
const result = await fetchJson(`${normalizedBase}/chat/completions`, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers,
|
|
130
|
+
body: {
|
|
131
|
+
model: resolvedModel,
|
|
132
|
+
messages,
|
|
133
|
+
stream: false
|
|
134
|
+
},
|
|
135
|
+
timeoutMs: request.timeoutMs ?? 120000,
|
|
136
|
+
fetchImpl
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (!result.ok) {
|
|
140
|
+
return invokeError(id, result.error ?? "Custom provider chat failed.");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const content = result.data?.choices?.[0]?.message?.content ?? "";
|
|
144
|
+
const usage = result.data?.usage ?? {};
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
ok: true,
|
|
148
|
+
backendId: id,
|
|
149
|
+
model: resolvedModel,
|
|
150
|
+
content,
|
|
151
|
+
usage: {
|
|
152
|
+
inputTokens: usage.prompt_tokens ?? null,
|
|
153
|
+
outputTokens: usage.completion_tokens ?? null,
|
|
154
|
+
cachedTokens: null,
|
|
155
|
+
estimatedCost: null,
|
|
156
|
+
model: resolvedModel,
|
|
157
|
+
backendId: id,
|
|
158
|
+
fallbackUsed: false
|
|
159
|
+
},
|
|
160
|
+
raw: result.data
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function invokeError(backendId, message) {
|
|
167
|
+
return {
|
|
168
|
+
ok: false,
|
|
169
|
+
backendId,
|
|
170
|
+
model: null,
|
|
171
|
+
content: null,
|
|
172
|
+
error: message,
|
|
173
|
+
usage: null
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BACKEND_IDS,
|
|
3
|
+
COST_CLASSES,
|
|
4
|
+
DEFAULT_OLLAMA_HOST,
|
|
5
|
+
PRIVACY_CLASSES,
|
|
6
|
+
createModelDescriptor
|
|
7
|
+
} from "../types.js";
|
|
8
|
+
import { fetchJson } from "../http.js";
|
|
9
|
+
import { CAPABILITY_STATES } from "../../capability-states.js";
|
|
10
|
+
|
|
11
|
+
export function createOllamaBackend({
|
|
12
|
+
host = null,
|
|
13
|
+
fetchImpl = globalThis.fetch,
|
|
14
|
+
env = process.env
|
|
15
|
+
} = {}) {
|
|
16
|
+
const baseUrl = normalizeOllamaHost(host ?? env.OLLAMA_HOST ?? DEFAULT_OLLAMA_HOST);
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
id: BACKEND_IDS.OLLAMA,
|
|
20
|
+
label: "Ollama",
|
|
21
|
+
local: true,
|
|
22
|
+
|
|
23
|
+
async detect() {
|
|
24
|
+
const result = await fetchJson(`${baseUrl}/api/tags`, {
|
|
25
|
+
timeoutMs: 2000,
|
|
26
|
+
fetchImpl
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (!result.ok) {
|
|
30
|
+
return {
|
|
31
|
+
id: BACKEND_IDS.OLLAMA,
|
|
32
|
+
label: "Ollama",
|
|
33
|
+
state: CAPABILITY_STATES.UNKNOWN,
|
|
34
|
+
detected: false,
|
|
35
|
+
available: false,
|
|
36
|
+
host: baseUrl,
|
|
37
|
+
error: result.error,
|
|
38
|
+
recommendation: `Start Ollama locally (${DEFAULT_OLLAMA_HOST}) or set OLLAMA_HOST.`
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const models = parseOllamaModels(result.data);
|
|
43
|
+
return {
|
|
44
|
+
id: BACKEND_IDS.OLLAMA,
|
|
45
|
+
label: "Ollama",
|
|
46
|
+
state: models.length > 0 ? CAPABILITY_STATES.AVAILABLE : CAPABILITY_STATES.DETECTED,
|
|
47
|
+
detected: true,
|
|
48
|
+
available: models.length > 0,
|
|
49
|
+
host: baseUrl,
|
|
50
|
+
modelCount: models.length,
|
|
51
|
+
error: null,
|
|
52
|
+
recommendation: models.length > 0
|
|
53
|
+
? `Ollama ready with ${models.length} local model(s).`
|
|
54
|
+
: "Ollama is running but no models are pulled yet."
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
async listModels() {
|
|
59
|
+
const result = await fetchJson(`${baseUrl}/api/tags`, { fetchImpl });
|
|
60
|
+
if (!result.ok) return [];
|
|
61
|
+
return parseOllamaModels(result.data);
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
async capabilities() {
|
|
65
|
+
const detection = await this.detect();
|
|
66
|
+
return {
|
|
67
|
+
id: BACKEND_IDS.OLLAMA,
|
|
68
|
+
local: true,
|
|
69
|
+
cloud: false,
|
|
70
|
+
requiresApiKey: false,
|
|
71
|
+
requiresConsent: false,
|
|
72
|
+
streaming: true,
|
|
73
|
+
tools: false,
|
|
74
|
+
state: detection.state,
|
|
75
|
+
host: baseUrl
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
async invoke(contextPack, request = {}) {
|
|
80
|
+
const modelId = request.modelId;
|
|
81
|
+
if (!modelId) {
|
|
82
|
+
return invokeError("Ollama invoke requires modelId.");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const messages = buildChatMessages(contextPack, request);
|
|
86
|
+
const result = await fetchJson(`${baseUrl}/api/chat`, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: { "Content-Type": "application/json" },
|
|
89
|
+
body: {
|
|
90
|
+
model: modelId,
|
|
91
|
+
messages,
|
|
92
|
+
stream: false,
|
|
93
|
+
options: request.options ?? undefined
|
|
94
|
+
},
|
|
95
|
+
timeoutMs: request.timeoutMs ?? 120000,
|
|
96
|
+
fetchImpl
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (!result.ok) {
|
|
100
|
+
return invokeError(result.error ?? "Ollama chat failed.");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const content = result.data?.message?.content ?? "";
|
|
104
|
+
return {
|
|
105
|
+
ok: true,
|
|
106
|
+
backendId: BACKEND_IDS.OLLAMA,
|
|
107
|
+
model: modelId,
|
|
108
|
+
content,
|
|
109
|
+
usage: {
|
|
110
|
+
inputTokens: result.data?.prompt_eval_count ?? null,
|
|
111
|
+
outputTokens: result.data?.eval_count ?? null,
|
|
112
|
+
cachedTokens: null,
|
|
113
|
+
estimatedCost: 0,
|
|
114
|
+
model: modelId,
|
|
115
|
+
backendId: BACKEND_IDS.OLLAMA,
|
|
116
|
+
fallbackUsed: false
|
|
117
|
+
},
|
|
118
|
+
raw: result.data
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function parseOllamaModels(data) {
|
|
125
|
+
const models = Array.isArray(data?.models) ? data.models : [];
|
|
126
|
+
return models.map((entry) => createModelDescriptor({
|
|
127
|
+
provider: BACKEND_IDS.OLLAMA,
|
|
128
|
+
modelId: entry.name ?? entry.model,
|
|
129
|
+
local: true,
|
|
130
|
+
costClass: COST_CLASSES.LOCAL,
|
|
131
|
+
privacyClass: PRIVACY_CLASSES.LOCAL,
|
|
132
|
+
contextLimit: null,
|
|
133
|
+
tools: false,
|
|
134
|
+
reasoning: false
|
|
135
|
+
})).filter((entry) => Boolean(entry.modelId));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function normalizeOllamaHost(host) {
|
|
139
|
+
return String(host).replace(/\/+$/, "");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function buildChatMessages(contextPack, request) {
|
|
143
|
+
const messages = [];
|
|
144
|
+
if (contextPack?.systemPrompt) {
|
|
145
|
+
messages.push({ role: "system", content: contextPack.systemPrompt });
|
|
146
|
+
}
|
|
147
|
+
if (Array.isArray(request.messages) && request.messages.length > 0) {
|
|
148
|
+
messages.push(...request.messages);
|
|
149
|
+
} else if (request.prompt) {
|
|
150
|
+
messages.push({ role: "user", content: request.prompt });
|
|
151
|
+
}
|
|
152
|
+
return messages;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function invokeError(message) {
|
|
156
|
+
return {
|
|
157
|
+
ok: false,
|
|
158
|
+
backendId: BACKEND_IDS.OLLAMA,
|
|
159
|
+
model: null,
|
|
160
|
+
content: null,
|
|
161
|
+
error: message,
|
|
162
|
+
usage: null
|
|
163
|
+
};
|
|
164
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BACKEND_IDS,
|
|
3
|
+
COST_CLASSES,
|
|
4
|
+
OPENROUTER_FREE_MODEL,
|
|
5
|
+
PRIVACY_CLASSES,
|
|
6
|
+
createModelDescriptor
|
|
7
|
+
} from "../types.js";
|
|
8
|
+
import { fetchJson } from "../http.js";
|
|
9
|
+
import { CAPABILITY_STATES } from "../../capability-states.js";
|
|
10
|
+
|
|
11
|
+
const OPENROUTER_BASE = "https://openrouter.ai/api/v1";
|
|
12
|
+
|
|
13
|
+
export function createOpenRouterBackend({
|
|
14
|
+
fetchImpl = globalThis.fetch,
|
|
15
|
+
env = process.env,
|
|
16
|
+
apiKey = null
|
|
17
|
+
} = {}) {
|
|
18
|
+
const resolvedKey = apiKey ?? env.OPENROUTER_API_KEY ?? null;
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
id: BACKEND_IDS.OPENROUTER,
|
|
22
|
+
label: "OpenRouter",
|
|
23
|
+
local: false,
|
|
24
|
+
|
|
25
|
+
async detect() {
|
|
26
|
+
if (!resolvedKey) {
|
|
27
|
+
return {
|
|
28
|
+
id: BACKEND_IDS.OPENROUTER,
|
|
29
|
+
label: "OpenRouter",
|
|
30
|
+
state: CAPABILITY_STATES.UNKNOWN,
|
|
31
|
+
detected: false,
|
|
32
|
+
available: false,
|
|
33
|
+
hasApiKey: false,
|
|
34
|
+
error: null,
|
|
35
|
+
recommendation: "Set OPENROUTER_API_KEY in the environment to enable OpenRouter (never stored by Kairo)."
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
id: BACKEND_IDS.OPENROUTER,
|
|
41
|
+
label: "OpenRouter",
|
|
42
|
+
state: CAPABILITY_STATES.AUTHENTICATED,
|
|
43
|
+
detected: true,
|
|
44
|
+
available: true,
|
|
45
|
+
hasApiKey: true,
|
|
46
|
+
error: null,
|
|
47
|
+
recommendation: "OpenRouter API key detected in environment. Cloud use still requires explicit consent."
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
async listModels({ freeOnly = true } = {}) {
|
|
52
|
+
if (!resolvedKey) return [];
|
|
53
|
+
|
|
54
|
+
const result = await fetchJson(`${OPENROUTER_BASE}/models`, {
|
|
55
|
+
headers: {
|
|
56
|
+
Authorization: `Bearer ${resolvedKey}`,
|
|
57
|
+
"Content-Type": "application/json"
|
|
58
|
+
},
|
|
59
|
+
fetchImpl
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!result.ok) {
|
|
63
|
+
return [
|
|
64
|
+
createModelDescriptor({
|
|
65
|
+
provider: BACKEND_IDS.OPENROUTER,
|
|
66
|
+
modelId: OPENROUTER_FREE_MODEL,
|
|
67
|
+
local: false,
|
|
68
|
+
costClass: COST_CLASSES.FREE,
|
|
69
|
+
privacyClass: PRIVACY_CLASSES.CLOUD,
|
|
70
|
+
opaque: true
|
|
71
|
+
})
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const models = Array.isArray(result.data?.data) ? result.data.data : [];
|
|
76
|
+
const mapped = models
|
|
77
|
+
.filter((entry) => {
|
|
78
|
+
if (!freeOnly) return true;
|
|
79
|
+
const id = entry.id ?? "";
|
|
80
|
+
return id.endsWith(":free") || id === OPENROUTER_FREE_MODEL;
|
|
81
|
+
})
|
|
82
|
+
.map((entry) => createModelDescriptor({
|
|
83
|
+
provider: BACKEND_IDS.OPENROUTER,
|
|
84
|
+
modelId: entry.id,
|
|
85
|
+
local: false,
|
|
86
|
+
costClass: COST_CLASSES.FREE,
|
|
87
|
+
privacyClass: PRIVACY_CLASSES.CLOUD,
|
|
88
|
+
contextLimit: entry.context_length ?? null,
|
|
89
|
+
tools: Boolean(entry.supported_parameters?.includes?.("tools")),
|
|
90
|
+
reasoning: false
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
if (!mapped.some((entry) => entry.modelId === OPENROUTER_FREE_MODEL)) {
|
|
94
|
+
mapped.unshift(createModelDescriptor({
|
|
95
|
+
provider: BACKEND_IDS.OPENROUTER,
|
|
96
|
+
modelId: OPENROUTER_FREE_MODEL,
|
|
97
|
+
local: false,
|
|
98
|
+
costClass: COST_CLASSES.FREE,
|
|
99
|
+
privacyClass: PRIVACY_CLASSES.CLOUD,
|
|
100
|
+
opaque: true
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return mapped;
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
async capabilities() {
|
|
108
|
+
const detection = await this.detect();
|
|
109
|
+
return {
|
|
110
|
+
id: BACKEND_IDS.OPENROUTER,
|
|
111
|
+
local: false,
|
|
112
|
+
cloud: true,
|
|
113
|
+
requiresApiKey: true,
|
|
114
|
+
requiresConsent: true,
|
|
115
|
+
streaming: true,
|
|
116
|
+
tools: true,
|
|
117
|
+
freeRouterModel: OPENROUTER_FREE_MODEL,
|
|
118
|
+
state: detection.state,
|
|
119
|
+
hasApiKey: Boolean(resolvedKey)
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
async invoke(contextPack, request = {}) {
|
|
124
|
+
if (!resolvedKey) {
|
|
125
|
+
return invokeError("OPENROUTER_API_KEY is not set. Kairo never stores credentials.");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const modelId = request.modelId ?? OPENROUTER_FREE_MODEL;
|
|
129
|
+
const messages = buildChatMessages(contextPack, request);
|
|
130
|
+
|
|
131
|
+
const result = await fetchJson(`${OPENROUTER_BASE}/chat/completions`, {
|
|
132
|
+
method: "POST",
|
|
133
|
+
headers: {
|
|
134
|
+
Authorization: `Bearer ${resolvedKey}`,
|
|
135
|
+
"Content-Type": "application/json",
|
|
136
|
+
"HTTP-Referer": "https://github.com/Kal-elSam/harness",
|
|
137
|
+
"X-OpenRouter-Title": "Kairo Runtime"
|
|
138
|
+
},
|
|
139
|
+
body: {
|
|
140
|
+
model: modelId,
|
|
141
|
+
messages,
|
|
142
|
+
stream: false
|
|
143
|
+
},
|
|
144
|
+
timeoutMs: request.timeoutMs ?? 120000,
|
|
145
|
+
fetchImpl
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (!result.ok) {
|
|
149
|
+
return invokeError(result.error ?? "OpenRouter chat failed.");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const content = result.data?.choices?.[0]?.message?.content ?? "";
|
|
153
|
+
const usedModel = result.data?.model ?? modelId;
|
|
154
|
+
const usage = result.data?.usage ?? {};
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
ok: true,
|
|
158
|
+
backendId: BACKEND_IDS.OPENROUTER,
|
|
159
|
+
model: usedModel,
|
|
160
|
+
content,
|
|
161
|
+
usage: {
|
|
162
|
+
inputTokens: usage.prompt_tokens ?? null,
|
|
163
|
+
outputTokens: usage.completion_tokens ?? null,
|
|
164
|
+
cachedTokens: usage.prompt_tokens_details?.cached_tokens ?? null,
|
|
165
|
+
estimatedCost: null,
|
|
166
|
+
model: usedModel,
|
|
167
|
+
backendId: BACKEND_IDS.OPENROUTER,
|
|
168
|
+
fallbackUsed: usedModel !== modelId
|
|
169
|
+
},
|
|
170
|
+
raw: result.data
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function buildChatMessages(contextPack, request) {
|
|
177
|
+
const messages = [];
|
|
178
|
+
if (contextPack?.systemPrompt) {
|
|
179
|
+
messages.push({ role: "system", content: contextPack.systemPrompt });
|
|
180
|
+
}
|
|
181
|
+
if (Array.isArray(request.messages) && request.messages.length > 0) {
|
|
182
|
+
messages.push(...request.messages);
|
|
183
|
+
} else if (request.prompt) {
|
|
184
|
+
messages.push({ role: "user", content: request.prompt });
|
|
185
|
+
}
|
|
186
|
+
return messages;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function invokeError(message) {
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
backendId: BACKEND_IDS.OPENROUTER,
|
|
193
|
+
model: null,
|
|
194
|
+
content: null,
|
|
195
|
+
error: message,
|
|
196
|
+
usage: null
|
|
197
|
+
};
|
|
198
|
+
}
|