titan-agent 6.4.3 → 6.5.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/dist/agent/agent.js +61 -46
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/agentLessons.js.map +1 -1
- package/dist/agent/agentLoop.js +5 -1
- package/dist/agent/agentLoop.js.map +1 -1
- package/dist/agent/orchestrator.js +5 -14
- package/dist/agent/orchestrator.js.map +1 -1
- package/dist/agent/pipeline.js +11 -6
- package/dist/agent/pipeline.js.map +1 -1
- package/dist/agent/somaInitiative.js.map +1 -1
- package/dist/agent/toolRunner.js +35 -0
- package/dist/agent/toolRunner.js.map +1 -1
- package/dist/channels/discord.js +10 -6
- package/dist/channels/discord.js.map +1 -1
- package/dist/channels/slack.js +7 -2
- package/dist/channels/slack.js.map +1 -1
- package/dist/channels/telegram.js +1 -1
- package/dist/channels/telegram.js.map +1 -1
- package/dist/config/schema.js +6 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/gateway/openai-compat.js +69 -10
- package/dist/gateway/openai-compat.js.map +1 -1
- package/dist/gateway/server.js +79 -0
- package/dist/gateway/server.js.map +1 -1
- package/dist/migrations/runner.js.map +1 -1
- package/dist/migrations/safeRun.js.map +1 -1
- package/dist/providers/anthropic.js +7 -4
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/base.js +8 -1
- package/dist/providers/base.js.map +1 -1
- package/dist/providers/google.js +7 -4
- package/dist/providers/google.js.map +1 -1
- package/dist/providers/ollama.js +8 -7
- package/dist/providers/ollama.js.map +1 -1
- package/dist/providers/openai.js +7 -4
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/openai_compat.js +7 -4
- package/dist/providers/openai_compat.js.map +1 -1
- package/dist/providers/router.js +7 -3
- package/dist/providers/router.js.map +1 -1
- package/dist/skills/builtin/backup.js.map +1 -1
- package/dist/skills/builtin/browser.js +11 -21
- package/dist/skills/builtin/browser.js.map +1 -1
- package/dist/skills/builtin/calendar.js +6 -2
- package/dist/skills/builtin/calendar.js.map +1 -1
- package/dist/skills/builtin/canvas_spaces.js.map +1 -1
- package/dist/skills/builtin/cron.js +7 -6
- package/dist/skills/builtin/cron.js.map +1 -1
- package/dist/skills/builtin/data_analysis.js +1 -1
- package/dist/skills/builtin/data_analysis.js.map +1 -1
- package/dist/skills/builtin/jira_linear.js +6 -4
- package/dist/skills/builtin/jira_linear.js.map +1 -1
- package/dist/skills/builtin/web_browser.js +6 -0
- package/dist/skills/builtin/web_browser.js.map +1 -1
- package/dist/skills/builtin/web_fetch.js +1 -0
- package/dist/skills/builtin/web_fetch.js.map +1 -1
- package/dist/storage/starterSpaces.js.map +1 -1
- package/dist/utils/constants.js +1 -1
- package/dist/utils/constants.js.map +1 -1
- package/package.json +1 -1
- package/ui/dist/sw.js +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Router } from "express";
|
|
3
|
-
import { chat, chatStream } from "../providers/router.js";
|
|
3
|
+
import { chat, chatStream, tryResolveModel, discoverAllModels, getKnownProviderNames } from "../providers/router.js";
|
|
4
4
|
import { loadConfig } from "../config/config.js";
|
|
5
5
|
import { v4 as uuid } from "uuid";
|
|
6
6
|
import logger from "../utils/logger.js";
|
|
@@ -24,7 +24,7 @@ function createOpenAICompatRouter() {
|
|
|
24
24
|
res.json({ object: "list", data: models });
|
|
25
25
|
});
|
|
26
26
|
router.post("/chat/completions", async (req, res) => {
|
|
27
|
-
const { model, messages, stream, temperature, max_tokens
|
|
27
|
+
const { model, messages, stream, temperature, max_tokens } = req.body;
|
|
28
28
|
if (!messages || !Array.isArray(messages)) {
|
|
29
29
|
res.status(400).json({ error: { message: "messages is required", type: "invalid_request_error" } });
|
|
30
30
|
return;
|
|
@@ -32,20 +32,59 @@ function createOpenAICompatRouter() {
|
|
|
32
32
|
const config = loadConfig();
|
|
33
33
|
const effectiveModel = model || config.agent.model;
|
|
34
34
|
const chatId = `chatcmpl-${uuid().slice(0, 12)}`;
|
|
35
|
-
|
|
35
|
+
if (model) {
|
|
36
|
+
const aliases = config.agent.modelAliases || {};
|
|
37
|
+
const aliased = aliases[effectiveModel] || effectiveModel;
|
|
38
|
+
const bareName = aliased.includes("/") ? aliased.split("/").slice(1).join("/") : aliased;
|
|
39
|
+
let known = false;
|
|
40
|
+
let availableIds = [];
|
|
41
|
+
try {
|
|
42
|
+
const discovered = await discoverAllModels();
|
|
43
|
+
availableIds = discovered.map((m) => m.id).slice(0, 50);
|
|
44
|
+
known = // a real, servable model — matched by full id or bare model name
|
|
45
|
+
discovered.some((m) => m.id === aliased || m.model === aliased || m.model === bareName) || aliased.includes("/") && getKnownProviderNames().includes(aliased.split("/")[0]);
|
|
46
|
+
} catch {
|
|
47
|
+
known = !!tryResolveModel(aliased);
|
|
48
|
+
}
|
|
49
|
+
if (!known) {
|
|
50
|
+
res.status(404).json({
|
|
51
|
+
error: {
|
|
52
|
+
message: `Model '${model}' is not recognized by TITAN: no configured alias, not a servable model, and no known provider prefix. See 'available_models'.`,
|
|
53
|
+
type: "model_not_found",
|
|
54
|
+
param: "model",
|
|
55
|
+
code: "model_not_found",
|
|
56
|
+
available_models: availableIds
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const routeInfo = tryResolveModel(effectiveModel);
|
|
63
|
+
const actualProvider = routeInfo?.provider?.name ?? "unknown";
|
|
64
|
+
const actualModelId = routeInfo ? `${routeInfo.provider.name}/${routeInfo.model}` : effectiveModel;
|
|
65
|
+
logger.info(COMPONENT, `Chat completion: requested=${effectiveModel} \u2192 routed=${actualModelId}, messages=${messages.length}, stream=${!!stream}`);
|
|
66
|
+
const ac = new AbortController();
|
|
67
|
+
req.on("close", () => ac.abort());
|
|
36
68
|
try {
|
|
37
69
|
if (stream) {
|
|
38
70
|
res.writeHead(200, {
|
|
39
71
|
"Content-Type": "text/event-stream",
|
|
40
72
|
"Cache-Control": "no-cache",
|
|
41
|
-
Connection: "keep-alive"
|
|
73
|
+
Connection: "keep-alive",
|
|
74
|
+
// v6.5 P0: cost transparency — surface the actual route
|
|
75
|
+
// even when the body says model='gpt-4'. Clients can read
|
|
76
|
+
// this header in their network logs / SDK middleware.
|
|
77
|
+
"X-Titan-Provider": actualProvider,
|
|
78
|
+
"X-Titan-Routed-Model": actualModelId,
|
|
79
|
+
"X-Titan-Requested-Model": effectiveModel
|
|
42
80
|
});
|
|
43
81
|
const sseWrite = setupSSEFlush(res);
|
|
44
82
|
for await (const chunk of chatStream({
|
|
45
83
|
model: effectiveModel,
|
|
46
84
|
messages,
|
|
47
85
|
temperature: temperature ?? config.agent.temperature,
|
|
48
|
-
maxTokens: max_tokens ?? config.agent.maxTokens
|
|
86
|
+
maxTokens: max_tokens ?? config.agent.maxTokens,
|
|
87
|
+
signal: ac.signal
|
|
49
88
|
})) {
|
|
50
89
|
if (chunk.type === "text" && chunk.content) {
|
|
51
90
|
const data = {
|
|
@@ -80,8 +119,12 @@ function createOpenAICompatRouter() {
|
|
|
80
119
|
model: effectiveModel,
|
|
81
120
|
messages,
|
|
82
121
|
temperature: temperature ?? config.agent.temperature,
|
|
83
|
-
maxTokens: max_tokens ?? config.agent.maxTokens
|
|
122
|
+
maxTokens: max_tokens ?? config.agent.maxTokens,
|
|
123
|
+
signal: ac.signal
|
|
84
124
|
});
|
|
125
|
+
res.setHeader("X-Titan-Provider", actualProvider);
|
|
126
|
+
res.setHeader("X-Titan-Routed-Model", actualModelId);
|
|
127
|
+
res.setHeader("X-Titan-Requested-Model", effectiveModel);
|
|
85
128
|
res.json({
|
|
86
129
|
id: chatId,
|
|
87
130
|
object: "chat.completion",
|
|
@@ -100,10 +143,26 @@ function createOpenAICompatRouter() {
|
|
|
100
143
|
});
|
|
101
144
|
}
|
|
102
145
|
} catch (err) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
146
|
+
const errMsg = err.message;
|
|
147
|
+
const errName = err.name;
|
|
148
|
+
const aborted = ac.signal.aborted || errName === "AbortError" || errName === "TimeoutError";
|
|
149
|
+
if (aborted) {
|
|
150
|
+
logger.info(COMPONENT, `Chat completion cancelled (client disconnect or timeout): ${errMsg}`);
|
|
151
|
+
} else {
|
|
152
|
+
logger.error(COMPONENT, `Chat completion error: ${errMsg}`);
|
|
153
|
+
}
|
|
154
|
+
if (res.headersSent) {
|
|
155
|
+
try {
|
|
156
|
+
res.end();
|
|
157
|
+
} catch {
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
res.setHeader("X-Titan-Provider", actualProvider);
|
|
161
|
+
res.setHeader("X-Titan-Error-Class", aborted ? "request_cancelled" : "server_error");
|
|
162
|
+
res.status(aborted ? 499 : 500).json({
|
|
163
|
+
error: { message: errMsg, type: aborted ? "request_cancelled" : "server_error" }
|
|
164
|
+
});
|
|
165
|
+
}
|
|
107
166
|
}
|
|
108
167
|
});
|
|
109
168
|
router.post("/embeddings", async (req, res) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/gateway/openai-compat.ts"],"sourcesContent":["/**\n * TITAN — OpenAI API Compatibility Layer\n * Serves /v1/models, /v1/chat/completions, /v1/embeddings so any OpenAI-compatible\n * client (OpenWebUI, Continue.dev, LiteLLM, etc.) can connect to TITAN as a provider.\n */\nimport { Router, type Request, type Response } from 'express';\nimport { chat, chatStream } from '../providers/router.js';\nimport { loadConfig } from '../config/config.js';\nimport { TITAN_VERSION } from '../utils/constants.js';\nimport { v4 as uuid } from 'uuid';\nimport logger from '../utils/logger.js';\nimport { setupSSEFlush } from '../utils/sseFlush.js';\n\nconst COMPONENT = 'OpenAI-Compat';\n\nexport function createOpenAICompatRouter(): Router {\n const router = Router();\n\n // GET /v1/models — list available models\n router.get('/models', (_req: Request, res: Response) => {\n const config = loadConfig();\n const currentModel = config.agent.model || 'unknown';\n const aliases = config.agent.modelAliases || {};\n\n const models = [\n { id: currentModel, object: 'model', created: Math.floor(Date.now() / 1000), owned_by: 'titan' },\n ...Object.entries(aliases)\n .filter(([, v]) => v && v !== currentModel)\n .map(([_key, value]) => ({\n id: value as string,\n object: 'model' as const,\n created: Math.floor(Date.now() / 1000),\n owned_by: 'titan',\n })),\n ];\n\n res.json({ object: 'list', data: models });\n });\n\n // POST /v1/chat/completions — chat completion (streaming and non-streaming)\n router.post('/chat/completions', async (req: Request, res: Response) => {\n const { model, messages, stream, temperature, max_tokens, top_p, stop } = req.body;\n\n if (!messages || !Array.isArray(messages)) {\n res.status(400).json({ error: { message: 'messages is required', type: 'invalid_request_error' } });\n return;\n }\n\n const config = loadConfig();\n const effectiveModel = model || config.agent.model;\n const chatId = `chatcmpl-${uuid().slice(0, 12)}`;\n\n logger.info(COMPONENT, `Chat completion: model=${effectiveModel}, messages=${messages.length}, stream=${!!stream}`);\n\n try {\n if (stream) {\n // SSE streaming response\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n });\n const sseWrite = setupSSEFlush(res);\n\n for await (const chunk of chatStream({\n model: effectiveModel,\n messages,\n temperature: temperature ?? config.agent.temperature,\n maxTokens: max_tokens ?? config.agent.maxTokens,\n })) {\n if (chunk.type === 'text' && chunk.content) {\n const data = {\n id: chatId,\n object: 'chat.completion.chunk',\n created: Math.floor(Date.now() / 1000),\n model: effectiveModel,\n choices: [{\n index: 0,\n delta: { content: chunk.content },\n finish_reason: null,\n }],\n };\n sseWrite(`data: ${JSON.stringify(data)}\\n\\n`);\n }\n }\n\n // Send final chunk\n sseWrite(`data: ${JSON.stringify({\n id: chatId,\n object: 'chat.completion.chunk',\n created: Math.floor(Date.now() / 1000),\n model: effectiveModel,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n })}\\n\\n`);\n sseWrite('data: [DONE]\\n\\n');\n res.end();\n } else {\n // Non-streaming response\n const response = await chat({\n model: effectiveModel,\n messages,\n temperature: temperature ?? config.agent.temperature,\n maxTokens: max_tokens ?? config.agent.maxTokens,\n });\n\n res.json({\n id: chatId,\n object: 'chat.completion',\n created: Math.floor(Date.now() / 1000),\n model: response.model || effectiveModel,\n choices: [{\n index: 0,\n message: { role: 'assistant', content: response.content },\n finish_reason: response.finishReason || 'stop',\n }],\n usage: {\n prompt_tokens: response.usage?.promptTokens ?? 0,\n completion_tokens: response.usage?.completionTokens ?? 0,\n total_tokens: response.usage?.totalTokens ?? 0,\n },\n });\n }\n } catch (err) {\n logger.error(COMPONENT, `Chat completion error: ${(err as Error).message}`);\n res.status(500).json({\n error: { message: (err as Error).message, type: 'server_error' },\n });\n }\n });\n\n // POST /v1/embeddings — embedding generation (proxy to Ollama)\n router.post('/embeddings', async (req: Request, res: Response) => {\n const { model, input } = req.body;\n const config = loadConfig();\n const embeddingModel = model || (config.memory as Record<string, unknown>)?.embeddingModel || 'nomic-embed-text';\n\n try {\n // Proxy to Ollama embeddings endpoint\n const ollamaUrl = config.providers?.ollama?.baseUrl || 'http://localhost:11434';\n const texts = Array.isArray(input) ? input : [input];\n const embeddings = [];\n\n for (let i = 0; i < texts.length; i++) {\n const resp = await fetch(`${ollamaUrl}/api/embed`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ model: embeddingModel, input: texts[i] }),\n });\n\n if (!resp.ok) throw new Error(`Ollama embed failed: ${resp.status}`);\n const data = await resp.json() as { embeddings?: number[][] };\n embeddings.push({\n object: 'embedding',\n index: i,\n embedding: data.embeddings?.[0] || [],\n });\n }\n\n res.json({\n object: 'list',\n data: embeddings,\n model: embeddingModel,\n usage: { prompt_tokens: 0, total_tokens: 0 },\n });\n } catch (err) {\n res.status(500).json({\n error: { message: (err as Error).message, type: 'server_error' },\n });\n }\n });\n\n return router;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/gateway/openai-compat.ts"],"sourcesContent":["/**\n * TITAN — OpenAI API Compatibility Layer\n * Serves /v1/models, /v1/chat/completions, /v1/embeddings so any OpenAI-compatible\n * client (OpenWebUI, Continue.dev, LiteLLM, etc.) can connect to TITAN as a provider.\n */\nimport { Router, type Request, type Response } from 'express';\nimport { chat, chatStream, tryResolveModel, discoverAllModels, getKnownProviderNames } from '../providers/router.js';\nimport { loadConfig } from '../config/config.js';\nimport { v4 as uuid } from 'uuid';\nimport logger from '../utils/logger.js';\nimport { setupSSEFlush } from '../utils/sseFlush.js';\n\nconst COMPONENT = 'OpenAI-Compat';\n\nexport function createOpenAICompatRouter(): Router {\n const router = Router();\n\n // GET /v1/models — list available models\n router.get('/models', (_req: Request, res: Response) => {\n const config = loadConfig();\n const currentModel = config.agent.model || 'unknown';\n const aliases = config.agent.modelAliases || {};\n\n const models = [\n { id: currentModel, object: 'model', created: Math.floor(Date.now() / 1000), owned_by: 'titan' },\n ...Object.entries(aliases)\n .filter(([, v]) => v && v !== currentModel)\n .map(([_key, value]) => ({\n id: value as string,\n object: 'model' as const,\n created: Math.floor(Date.now() / 1000),\n owned_by: 'titan',\n })),\n ];\n\n res.json({ object: 'list', data: models });\n });\n\n // POST /v1/chat/completions — chat completion (streaming and non-streaming)\n router.post('/chat/completions', async (req: Request, res: Response) => {\n const { model, messages, stream, temperature, max_tokens } = req.body;\n\n if (!messages || !Array.isArray(messages)) {\n res.status(400).json({ error: { message: 'messages is required', type: 'invalid_request_error' } });\n return;\n }\n\n const config = loadConfig();\n const effectiveModel = model || config.agent.model;\n const chatId = `chatcmpl-${uuid().slice(0, 12)}`;\n\n // ── v6.5 P2: validate model is recognized BEFORE generating ──\n // Sage flagged this during the v6.5 Council session: silent fallback to\n // config.agent.model when the client sends a model TITAN doesn't know\n // (e.g. 'claude-3.7-sonnet') is a trust violation — the client thinks\n // they got 3.7-sonnet's output. Return 404 with the recognized models\n // list instead. Skip this check when the client omitted `model`\n // entirely (using TITAN's default is fine).\n if (model) {\n // v6.5 fix — the old guard used tryResolveModel(), which RESOLVES a\n // bare id like 'gpt-4'/'claude-3.7-sonnet' to anthropic/<literal> and\n // returns truthy, so the 404 only fired for ids with an explicit\n // UNKNOWN provider prefix ('foo/bar'). The exact deception P2 targets\n // (bare 'claude-3.7-sonnet' → silent mis-route → Anthropic 500) slipped\n // through. Validate against the REAL servable-model set instead.\n const aliases = (config.agent.modelAliases || {}) as Record<string, string>;\n const aliased = aliases[effectiveModel] || effectiveModel;\n const bareName = aliased.includes('/') ? aliased.split('/').slice(1).join('/') : aliased;\n\n let known = false;\n let availableIds: string[] = [];\n try {\n const discovered = await discoverAllModels();\n availableIds = discovered.map(m => m.id).slice(0, 50);\n known =\n // a real, servable model — matched by full id or bare model name\n discovered.some(m => m.id === aliased || m.model === aliased || m.model === bareName)\n // OR an explicit, KNOWN provider prefix — the client deliberately\n // chose a provider, so let that provider validate the model name.\n || (aliased.includes('/') && getKnownProviderNames().includes(aliased.split('/')[0]));\n } catch {\n // Discovery failed — we can't prove the model is unknown, so fall\n // back to the old resolve check rather than block a valid request.\n known = !!tryResolveModel(aliased);\n }\n\n if (!known) {\n res.status(404).json({\n error: {\n message: `Model '${model}' is not recognized by TITAN: no configured alias, not a servable model, and no known provider prefix. See 'available_models'.`,\n type: 'model_not_found',\n param: 'model',\n code: 'model_not_found',\n available_models: availableIds,\n },\n });\n return;\n }\n }\n\n // ── v6.5 P0: cost transparency headers ──\n // Sage flagged the cost-deception bug: if client sends 'gpt-4' + OpenAI\n // key and TITAN routes to ollama/qwen, the client's OpenAI dashboard\n // never moves. Set x-titan-provider so the actual route is at least\n // VISIBLE in network logs even when the body says model='gpt-4'.\n const routeInfo = tryResolveModel(effectiveModel);\n const actualProvider = routeInfo?.provider?.name ?? 'unknown';\n const actualModelId = routeInfo ? `${routeInfo.provider.name}/${routeInfo.model}` : effectiveModel;\n\n logger.info(COMPONENT, `Chat completion: requested=${effectiveModel} → routed=${actualModelId}, messages=${messages.length}, stream=${!!stream}`);\n\n // v6.5 P1 — cancel upstream generation when the client disconnects.\n // Cline/Continue/Aider give up after 30-60s while a TITAN skill loop can\n // run for minutes; without this we keep burning provider tokens to a dead\n // socket. The signal flows chat/chatStream → router → provider fetch().\n const ac = new AbortController();\n req.on('close', () => ac.abort());\n\n try {\n if (stream) {\n // SSE streaming response\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n // v6.5 P0: cost transparency — surface the actual route\n // even when the body says model='gpt-4'. Clients can read\n // this header in their network logs / SDK middleware.\n 'X-Titan-Provider': actualProvider,\n 'X-Titan-Routed-Model': actualModelId,\n 'X-Titan-Requested-Model': effectiveModel,\n });\n const sseWrite = setupSSEFlush(res);\n\n for await (const chunk of chatStream({\n model: effectiveModel,\n messages,\n temperature: temperature ?? config.agent.temperature,\n maxTokens: max_tokens ?? config.agent.maxTokens,\n signal: ac.signal,\n })) {\n if (chunk.type === 'text' && chunk.content) {\n const data = {\n id: chatId,\n object: 'chat.completion.chunk',\n created: Math.floor(Date.now() / 1000),\n model: effectiveModel,\n choices: [{\n index: 0,\n delta: { content: chunk.content },\n finish_reason: null,\n }],\n };\n sseWrite(`data: ${JSON.stringify(data)}\\n\\n`);\n }\n }\n\n // Send final chunk\n sseWrite(`data: ${JSON.stringify({\n id: chatId,\n object: 'chat.completion.chunk',\n created: Math.floor(Date.now() / 1000),\n model: effectiveModel,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n })}\\n\\n`);\n sseWrite('data: [DONE]\\n\\n');\n res.end();\n } else {\n // Non-streaming response\n const response = await chat({\n model: effectiveModel,\n messages,\n temperature: temperature ?? config.agent.temperature,\n maxTokens: max_tokens ?? config.agent.maxTokens,\n signal: ac.signal,\n });\n\n // v6.5 P0: cost transparency headers on the non-stream path too\n res.setHeader('X-Titan-Provider', actualProvider);\n res.setHeader('X-Titan-Routed-Model', actualModelId);\n res.setHeader('X-Titan-Requested-Model', effectiveModel);\n\n res.json({\n id: chatId,\n object: 'chat.completion',\n created: Math.floor(Date.now() / 1000),\n model: response.model || effectiveModel,\n choices: [{\n index: 0,\n message: { role: 'assistant', content: response.content },\n finish_reason: response.finishReason || 'stop',\n }],\n usage: {\n prompt_tokens: response.usage?.promptTokens ?? 0,\n completion_tokens: response.usage?.completionTokens ?? 0,\n total_tokens: response.usage?.totalTokens ?? 0,\n },\n });\n }\n } catch (err) {\n // v6.5 P1/P3 — distinguish client-cancellation from real failures, and\n // never throw ERR_HTTP_HEADERS_SENT on the streaming path. If the client\n // disconnected (ac aborted) or the stream timed out, that's expected\n // cancellation, not a server error — there's nothing to write to a dead\n // socket. Once SSE headers are sent we can't change the status code, so\n // just close the stream cleanly. X-Titan-Error-Class gives clients a\n // back-channel for the non-streaming case.\n const errMsg = (err as Error).message;\n const errName = (err as Error).name;\n const aborted = ac.signal.aborted || errName === 'AbortError' || errName === 'TimeoutError';\n if (aborted) {\n logger.info(COMPONENT, `Chat completion cancelled (client disconnect or timeout): ${errMsg}`);\n } else {\n logger.error(COMPONENT, `Chat completion error: ${errMsg}`);\n }\n if (res.headersSent) {\n // streaming path — terminate the SSE stream without a status change\n try { res.end(); } catch { /* socket already gone */ }\n } else {\n res.setHeader('X-Titan-Provider', actualProvider);\n res.setHeader('X-Titan-Error-Class', aborted ? 'request_cancelled' : 'server_error');\n res.status(aborted ? 499 : 500).json({\n error: { message: errMsg, type: aborted ? 'request_cancelled' : 'server_error' },\n });\n }\n }\n });\n\n // POST /v1/embeddings — embedding generation (proxy to Ollama)\n router.post('/embeddings', async (req: Request, res: Response) => {\n const { model, input } = req.body;\n const config = loadConfig();\n const embeddingModel = model || (config.memory as Record<string, unknown>)?.embeddingModel || 'nomic-embed-text';\n\n try {\n // Proxy to Ollama embeddings endpoint\n const ollamaUrl = config.providers?.ollama?.baseUrl || 'http://localhost:11434';\n const texts = Array.isArray(input) ? input : [input];\n const embeddings = [];\n\n for (let i = 0; i < texts.length; i++) {\n const resp = await fetch(`${ollamaUrl}/api/embed`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ model: embeddingModel, input: texts[i] }),\n });\n\n if (!resp.ok) throw new Error(`Ollama embed failed: ${resp.status}`);\n const data = await resp.json() as { embeddings?: number[][] };\n embeddings.push({\n object: 'embedding',\n index: i,\n embedding: data.embeddings?.[0] || [],\n });\n }\n\n res.json({\n object: 'list',\n data: embeddings,\n model: embeddingModel,\n usage: { prompt_tokens: 0, total_tokens: 0 },\n });\n } catch (err) {\n res.status(500).json({\n error: { message: (err as Error).message, type: 'server_error' },\n });\n }\n });\n\n return router;\n}\n"],"mappings":";AAKA,SAAS,cAA2C;AACpD,SAAS,MAAM,YAAY,iBAAiB,mBAAmB,6BAA6B;AAC5F,SAAS,kBAAkB;AAC3B,SAAS,MAAM,YAAY;AAC3B,OAAO,YAAY;AACnB,SAAS,qBAAqB;AAE9B,MAAM,YAAY;AAEX,SAAS,2BAAmC;AAC/C,QAAM,SAAS,OAAO;AAGtB,SAAO,IAAI,WAAW,CAAC,MAAe,QAAkB;AACpD,UAAM,SAAS,WAAW;AAC1B,UAAM,eAAe,OAAO,MAAM,SAAS;AAC3C,UAAM,UAAU,OAAO,MAAM,gBAAgB,CAAC;AAE9C,UAAM,SAAS;AAAA,MACX,EAAE,IAAI,cAAc,QAAQ,SAAS,SAAS,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,GAAG,UAAU,QAAQ;AAAA,MAC/F,GAAG,OAAO,QAAQ,OAAO,EACpB,OAAO,CAAC,CAAC,EAAE,CAAC,MAAM,KAAK,MAAM,YAAY,EACzC,IAAI,CAAC,CAAC,MAAM,KAAK,OAAO;AAAA,QACrB,IAAI;AAAA,QACJ,QAAQ;AAAA,QACR,SAAS,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAAA,QACrC,UAAU;AAAA,MACd,EAAE;AAAA,IACV;AAEA,QAAI,KAAK,EAAE,QAAQ,QAAQ,MAAM,OAAO,CAAC;AAAA,EAC7C,CAAC;AAGD,SAAO,KAAK,qBAAqB,OAAO,KAAc,QAAkB;AACpE,UAAM,EAAE,OAAO,UAAU,QAAQ,aAAa,WAAW,IAAI,IAAI;AAEjE,QAAI,CAAC,YAAY,CAAC,MAAM,QAAQ,QAAQ,GAAG;AACvC,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,wBAAwB,MAAM,wBAAwB,EAAE,CAAC;AAClG;AAAA,IACJ;AAEA,UAAM,SAAS,WAAW;AAC1B,UAAM,iBAAiB,SAAS,OAAO,MAAM;AAC7C,UAAM,SAAS,YAAY,KAAK,EAAE,MAAM,GAAG,EAAE,CAAC;AAS9C,QAAI,OAAO;AAOP,YAAM,UAAW,OAAO,MAAM,gBAAgB,CAAC;AAC/C,YAAM,UAAU,QAAQ,cAAc,KAAK;AAC3C,YAAM,WAAW,QAAQ,SAAS,GAAG,IAAI,QAAQ,MAAM,GAAG,EAAE,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI;AAEjF,UAAI,QAAQ;AACZ,UAAI,eAAyB,CAAC;AAC9B,UAAI;AACA,cAAM,aAAa,MAAM,kBAAkB;AAC3C,uBAAe,WAAW,IAAI,OAAK,EAAE,EAAE,EAAE,MAAM,GAAG,EAAE;AACpD;AAAA,QAEI,WAAW,KAAK,OAAK,EAAE,OAAO,WAAW,EAAE,UAAU,WAAW,EAAE,UAAU,QAAQ,KAGhF,QAAQ,SAAS,GAAG,KAAK,sBAAsB,EAAE,SAAS,QAAQ,MAAM,GAAG,EAAE,CAAC,CAAC;AAAA,MAC3F,QAAQ;AAGJ,gBAAQ,CAAC,CAAC,gBAAgB,OAAO;AAAA,MACrC;AAEA,UAAI,CAAC,OAAO;AACR,YAAI,OAAO,GAAG,EAAE,KAAK;AAAA,UACjB,OAAO;AAAA,YACH,SAAS,UAAU,KAAK;AAAA,YACxB,MAAM;AAAA,YACN,OAAO;AAAA,YACP,MAAM;AAAA,YACN,kBAAkB;AAAA,UACtB;AAAA,QACJ,CAAC;AACD;AAAA,MACJ;AAAA,IACJ;AAOA,UAAM,YAAY,gBAAgB,cAAc;AAChD,UAAM,iBAAiB,WAAW,UAAU,QAAQ;AACpD,UAAM,gBAAgB,YAAY,GAAG,UAAU,SAAS,IAAI,IAAI,UAAU,KAAK,KAAK;AAEpF,WAAO,KAAK,WAAW,8BAA8B,cAAc,kBAAa,aAAa,cAAc,SAAS,MAAM,YAAY,CAAC,CAAC,MAAM,EAAE;AAMhJ,UAAM,KAAK,IAAI,gBAAgB;AAC/B,QAAI,GAAG,SAAS,MAAM,GAAG,MAAM,CAAC;AAEhC,QAAI;AACA,UAAI,QAAQ;AAER,YAAI,UAAU,KAAK;AAAA,UACf,gBAAgB;AAAA,UAChB,iBAAiB;AAAA,UACjB,YAAY;AAAA;AAAA;AAAA;AAAA,UAIZ,oBAAoB;AAAA,UACpB,wBAAwB;AAAA,UACxB,2BAA2B;AAAA,QAC/B,CAAC;AACD,cAAM,WAAW,cAAc,GAAG;AAElC,yBAAiB,SAAS,WAAW;AAAA,UACjC,OAAO;AAAA,UACP;AAAA,UACA,aAAa,eAAe,OAAO,MAAM;AAAA,UACzC,WAAW,cAAc,OAAO,MAAM;AAAA,UACtC,QAAQ,GAAG;AAAA,QACf,CAAC,GAAG;AACA,cAAI,MAAM,SAAS,UAAU,MAAM,SAAS;AACxC,kBAAM,OAAO;AAAA,cACT,IAAI;AAAA,cACJ,QAAQ;AAAA,cACR,SAAS,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAAA,cACrC,OAAO;AAAA,cACP,SAAS,CAAC;AAAA,gBACN,OAAO;AAAA,gBACP,OAAO,EAAE,SAAS,MAAM,QAAQ;AAAA,gBAChC,eAAe;AAAA,cACnB,CAAC;AAAA,YACL;AACA,qBAAS,SAAS,KAAK,UAAU,IAAI,CAAC;AAAA;AAAA,CAAM;AAAA,UAChD;AAAA,QACJ;AAGA,iBAAS,SAAS,KAAK,UAAU;AAAA,UAC7B,IAAI;AAAA,UACJ,QAAQ;AAAA,UACR,SAAS,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAAA,UACrC,OAAO;AAAA,UACP,SAAS,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,GAAG,eAAe,OAAO,CAAC;AAAA,QAC5D,CAAC,CAAC;AAAA;AAAA,CAAM;AACR,iBAAS,kBAAkB;AAC3B,YAAI,IAAI;AAAA,MACZ,OAAO;AAEH,cAAM,WAAW,MAAM,KAAK;AAAA,UACxB,OAAO;AAAA,UACP;AAAA,UACA,aAAa,eAAe,OAAO,MAAM;AAAA,UACzC,WAAW,cAAc,OAAO,MAAM;AAAA,UACtC,QAAQ,GAAG;AAAA,QACf,CAAC;AAGD,YAAI,UAAU,oBAAoB,cAAc;AAChD,YAAI,UAAU,wBAAwB,aAAa;AACnD,YAAI,UAAU,2BAA2B,cAAc;AAEvD,YAAI,KAAK;AAAA,UACL,IAAI;AAAA,UACJ,QAAQ;AAAA,UACR,SAAS,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAAA,UACrC,OAAO,SAAS,SAAS;AAAA,UACzB,SAAS,CAAC;AAAA,YACN,OAAO;AAAA,YACP,SAAS,EAAE,MAAM,aAAa,SAAS,SAAS,QAAQ;AAAA,YACxD,eAAe,SAAS,gBAAgB;AAAA,UAC5C,CAAC;AAAA,UACD,OAAO;AAAA,YACH,eAAe,SAAS,OAAO,gBAAgB;AAAA,YAC/C,mBAAmB,SAAS,OAAO,oBAAoB;AAAA,YACvD,cAAc,SAAS,OAAO,eAAe;AAAA,UACjD;AAAA,QACJ,CAAC;AAAA,MACL;AAAA,IACJ,SAAS,KAAK;AAQV,YAAM,SAAU,IAAc;AAC9B,YAAM,UAAW,IAAc;AAC/B,YAAM,UAAU,GAAG,OAAO,WAAW,YAAY,gBAAgB,YAAY;AAC7E,UAAI,SAAS;AACT,eAAO,KAAK,WAAW,6DAA6D,MAAM,EAAE;AAAA,MAChG,OAAO;AACH,eAAO,MAAM,WAAW,0BAA0B,MAAM,EAAE;AAAA,MAC9D;AACA,UAAI,IAAI,aAAa;AAEjB,YAAI;AAAE,cAAI,IAAI;AAAA,QAAG,QAAQ;AAAA,QAA4B;AAAA,MACzD,OAAO;AACH,YAAI,UAAU,oBAAoB,cAAc;AAChD,YAAI,UAAU,uBAAuB,UAAU,sBAAsB,cAAc;AACnF,YAAI,OAAO,UAAU,MAAM,GAAG,EAAE,KAAK;AAAA,UACjC,OAAO,EAAE,SAAS,QAAQ,MAAM,UAAU,sBAAsB,eAAe;AAAA,QACnF,CAAC;AAAA,MACL;AAAA,IACJ;AAAA,EACJ,CAAC;AAGD,SAAO,KAAK,eAAe,OAAO,KAAc,QAAkB;AAC9D,UAAM,EAAE,OAAO,MAAM,IAAI,IAAI;AAC7B,UAAM,SAAS,WAAW;AAC1B,UAAM,iBAAiB,SAAU,OAAO,QAAoC,kBAAkB;AAE9F,QAAI;AAEA,YAAM,YAAY,OAAO,WAAW,QAAQ,WAAW;AACvD,YAAM,QAAQ,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK;AACnD,YAAM,aAAa,CAAC;AAEpB,eAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACnC,cAAM,OAAO,MAAM,MAAM,GAAG,SAAS,cAAc;AAAA,UAC/C,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,UAC9C,MAAM,KAAK,UAAU,EAAE,OAAO,gBAAgB,OAAO,MAAM,CAAC,EAAE,CAAC;AAAA,QACnE,CAAC;AAED,YAAI,CAAC,KAAK,GAAI,OAAM,IAAI,MAAM,wBAAwB,KAAK,MAAM,EAAE;AACnE,cAAM,OAAO,MAAM,KAAK,KAAK;AAC7B,mBAAW,KAAK;AAAA,UACZ,QAAQ;AAAA,UACR,OAAO;AAAA,UACP,WAAW,KAAK,aAAa,CAAC,KAAK,CAAC;AAAA,QACxC,CAAC;AAAA,MACL;AAEA,UAAI,KAAK;AAAA,QACL,QAAQ;AAAA,QACR,MAAM;AAAA,QACN,OAAO;AAAA,QACP,OAAO,EAAE,eAAe,GAAG,cAAc,EAAE;AAAA,MAC/C,CAAC;AAAA,IACL,SAAS,KAAK;AACV,UAAI,OAAO,GAAG,EAAE,KAAK;AAAA,QACjB,OAAO,EAAE,SAAU,IAAc,SAAS,MAAM,eAAe;AAAA,MACnE,CAAC;AAAA,IACL;AAAA,EACJ,CAAC;AAED,SAAO;AACX;","names":[]}
|
package/dist/gateway/server.js
CHANGED
|
@@ -33,6 +33,7 @@ import { MattermostChannel } from "../channels/mattermost.js";
|
|
|
33
33
|
import { LarkChannel } from "../channels/lark.js";
|
|
34
34
|
import { EmailInboundChannel } from "../channels/email_inbound.js";
|
|
35
35
|
import { LineChannel } from "../channels/line.js";
|
|
36
|
+
import { kill as killSwitchFire, resume as killSwitchResume, getState as getKillSwitchState } from "../safety/killSwitch.js";
|
|
36
37
|
import { ZulipChannel } from "../channels/zulip.js";
|
|
37
38
|
import { MessengerChannel } from "../channels/messenger.js";
|
|
38
39
|
import { initAgents, routeMessage, listAgents } from "../agent/multiAgent.js";
|
|
@@ -835,6 +836,31 @@ async function startGateway(options) {
|
|
|
835
836
|
});
|
|
836
837
|
next();
|
|
837
838
|
});
|
|
839
|
+
app.use("/v1", (req, res, next) => {
|
|
840
|
+
const cfg = loadConfig();
|
|
841
|
+
const auth = cfg.gateway.auth;
|
|
842
|
+
if (!auth || auth.mode === "none") {
|
|
843
|
+
next();
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
if (auth.mode === "token" && !auth.token) {
|
|
847
|
+
next();
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
const header = req.headers.authorization;
|
|
851
|
+
const token = header?.startsWith("Bearer ") ? header.slice(7) : req.query.token;
|
|
852
|
+
if (isValidToken(token, cfg)) {
|
|
853
|
+
next();
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
res.status(401).json({
|
|
857
|
+
error: {
|
|
858
|
+
message: "Unauthorized. /v1 requires the TITAN gateway token as the Bearer credential when auth is enabled.",
|
|
859
|
+
type: "invalid_request_error",
|
|
860
|
+
code: "unauthorized"
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
});
|
|
838
864
|
app.use("/v1", createOpenAICompatRouter());
|
|
839
865
|
app.use("/api/paperclip", createPaperclipRouter());
|
|
840
866
|
app.use("/paperclip", createPaperclipUIRouter());
|
|
@@ -1002,6 +1028,14 @@ async function startGateway(options) {
|
|
|
1002
1028
|
next();
|
|
1003
1029
|
return;
|
|
1004
1030
|
}
|
|
1031
|
+
if (req.path === "/channels/line/webhook") {
|
|
1032
|
+
next();
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
if (req.path === "/channels/lark/webhook") {
|
|
1036
|
+
next();
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1005
1039
|
const header = req.headers.authorization;
|
|
1006
1040
|
const token = header?.startsWith("Bearer ") ? header.slice(7) : req.query.token;
|
|
1007
1041
|
if (isValidToken(token, cfg)) {
|
|
@@ -3571,6 +3605,51 @@ data: ${JSON.stringify(structured)}
|
|
|
3571
3605
|
});
|
|
3572
3606
|
logger.info(COMPONENT, `Messenger webhook: /api/messenger/webhook + /messenger/webhook (verify token: ${messengerAdapter.getVerifyToken()})`);
|
|
3573
3607
|
}
|
|
3608
|
+
const lineAdapter = channels.get("line");
|
|
3609
|
+
if (lineAdapter) {
|
|
3610
|
+
app.post("/api/channels/line/webhook", express.json(), (req, res) => {
|
|
3611
|
+
try {
|
|
3612
|
+
lineAdapter.handleWebhookEvent(req.body || {});
|
|
3613
|
+
} catch (e) {
|
|
3614
|
+
logger.error(COMPONENT, `LINE webhook error: ${e.message}`);
|
|
3615
|
+
}
|
|
3616
|
+
res.status(200).end();
|
|
3617
|
+
});
|
|
3618
|
+
logger.info(COMPONENT, "LINE webhook: POST /api/channels/line/webhook");
|
|
3619
|
+
}
|
|
3620
|
+
const larkAdapter = channels.get("lark");
|
|
3621
|
+
if (larkAdapter) {
|
|
3622
|
+
app.post("/api/channels/lark/webhook", express.json(), (req, res) => {
|
|
3623
|
+
try {
|
|
3624
|
+
const result = larkAdapter.handleWebhookEvent(req.body || {});
|
|
3625
|
+
if (result?.challenge) {
|
|
3626
|
+
res.json({ challenge: result.challenge });
|
|
3627
|
+
return;
|
|
3628
|
+
}
|
|
3629
|
+
} catch (e) {
|
|
3630
|
+
logger.error(COMPONENT, `Lark webhook error: ${e.message}`);
|
|
3631
|
+
}
|
|
3632
|
+
res.status(200).end();
|
|
3633
|
+
});
|
|
3634
|
+
logger.info(COMPONENT, "Lark webhook: POST /api/channels/lark/webhook");
|
|
3635
|
+
}
|
|
3636
|
+
app.get("/api/safety/kill-switch", (_req, res) => {
|
|
3637
|
+
res.json(getKillSwitchState());
|
|
3638
|
+
});
|
|
3639
|
+
app.post("/api/safety/kill", express.json(), async (req, res) => {
|
|
3640
|
+
const reason = req.body?.reason || "Manual kill via API";
|
|
3641
|
+
try {
|
|
3642
|
+
await killSwitchFire("manual", reason, { firedBy: req.body?.firedBy || "api" });
|
|
3643
|
+
res.json({ ok: true, state: getKillSwitchState() });
|
|
3644
|
+
} catch (e) {
|
|
3645
|
+
res.status(500).json({ error: e.message });
|
|
3646
|
+
}
|
|
3647
|
+
});
|
|
3648
|
+
app.post("/api/safety/resume", express.json(), (req, res) => {
|
|
3649
|
+
const note = req.body?.resolutionNote || req.body?.reason || "Resumed via API";
|
|
3650
|
+
const state = killSwitchResume(note, req.body?.resumedBy || "api");
|
|
3651
|
+
res.json({ ok: true, state });
|
|
3652
|
+
});
|
|
3574
3653
|
{
|
|
3575
3654
|
const {
|
|
3576
3655
|
twimlPlayAndGather,
|