skyloom 1.4.5 → 1.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/config/default.yaml +47 -0
- package/config/providers.yaml +39 -0
- package/package.json +1 -1
- package/src/core/llm.ts +111 -45
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Skyloom default configuration
|
|
2
|
+
llm:
|
|
3
|
+
default_model: gpt-4o
|
|
4
|
+
language: zh
|
|
5
|
+
max_retries: 2
|
|
6
|
+
temperature: 0.7
|
|
7
|
+
max_tokens: 4096
|
|
8
|
+
|
|
9
|
+
agents:
|
|
10
|
+
fog:
|
|
11
|
+
model: gpt-4o
|
|
12
|
+
temperature: 0.7
|
|
13
|
+
rain:
|
|
14
|
+
model: gpt-4o
|
|
15
|
+
temperature: 0.7
|
|
16
|
+
frost:
|
|
17
|
+
model: gpt-4o
|
|
18
|
+
temperature: 0.3
|
|
19
|
+
snow:
|
|
20
|
+
model: gpt-4o
|
|
21
|
+
temperature: 0.5
|
|
22
|
+
dew:
|
|
23
|
+
model: gpt-4o
|
|
24
|
+
temperature: 0.3
|
|
25
|
+
fair:
|
|
26
|
+
model: gpt-4o
|
|
27
|
+
temperature: 0.9
|
|
28
|
+
|
|
29
|
+
memory:
|
|
30
|
+
db_path: ~/.skyloom/memory.db
|
|
31
|
+
short_term_limit: 100
|
|
32
|
+
max_persisted_messages: 2000
|
|
33
|
+
|
|
34
|
+
workspace:
|
|
35
|
+
path: auto
|
|
36
|
+
|
|
37
|
+
cli:
|
|
38
|
+
default_agent: fog
|
|
39
|
+
approval_mode: interactive
|
|
40
|
+
|
|
41
|
+
plugins:
|
|
42
|
+
enabled: true
|
|
43
|
+
directories:
|
|
44
|
+
- ~/.skyloom/plugins
|
|
45
|
+
|
|
46
|
+
mcp:
|
|
47
|
+
servers: []
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Provider catalog — API key env vars, base URLs, docs
|
|
2
|
+
openai:
|
|
3
|
+
env_var: OPENAI_API_KEY
|
|
4
|
+
base_url: https://api.openai.com/v1
|
|
5
|
+
docs_url: https://platform.openai.com/api-keys
|
|
6
|
+
|
|
7
|
+
anthropic:
|
|
8
|
+
env_var: ANTHROPIC_API_KEY
|
|
9
|
+
base_url: https://api.anthropic.com/v1
|
|
10
|
+
docs_url: https://console.anthropic.com/settings/keys
|
|
11
|
+
|
|
12
|
+
deepseek:
|
|
13
|
+
env_var: DEEPSEEK_API_KEY
|
|
14
|
+
base_url: https://api.deepseek.com/v1
|
|
15
|
+
docs_url: https://platform.deepseek.com/api_keys
|
|
16
|
+
|
|
17
|
+
groq:
|
|
18
|
+
env_var: GROQ_API_KEY
|
|
19
|
+
base_url: https://api.groq.com/openai/v1
|
|
20
|
+
|
|
21
|
+
mistral:
|
|
22
|
+
env_var: MISTRAL_API_KEY
|
|
23
|
+
base_url: https://api.mistral.ai/v1
|
|
24
|
+
|
|
25
|
+
cohere:
|
|
26
|
+
env_var: COHERE_API_KEY
|
|
27
|
+
base_url: https://api.cohere.ai/v1
|
|
28
|
+
|
|
29
|
+
openrouter:
|
|
30
|
+
env_var: OPENROUTER_API_KEY
|
|
31
|
+
base_url: https://openrouter.ai/api/v1
|
|
32
|
+
|
|
33
|
+
gemini:
|
|
34
|
+
env_var: GEMINI_API_KEY
|
|
35
|
+
base_url: https://generativelanguage.googleapis.com/v1beta
|
|
36
|
+
|
|
37
|
+
ollama:
|
|
38
|
+
base_url: http://localhost:11434/v1
|
|
39
|
+
env_var: OLLAMA_HOST
|
package/package.json
CHANGED
package/src/core/llm.ts
CHANGED
|
@@ -662,63 +662,129 @@ export class LLMClient {
|
|
|
662
662
|
}
|
|
663
663
|
|
|
664
664
|
/**
|
|
665
|
-
* Complete with retry logic
|
|
665
|
+
* Complete with retry logic — real HTTP call to LLM API.
|
|
666
666
|
*/
|
|
667
667
|
private async completeWithRetry(
|
|
668
668
|
model: string,
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
669
|
+
messages: Record<string, unknown>[],
|
|
670
|
+
agentName?: string,
|
|
671
|
+
tools?: string[],
|
|
672
|
+
stream: boolean = false,
|
|
673
673
|
overrides?: Record<string, unknown>
|
|
674
674
|
): Promise<LLMResponse> {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
675
|
+
const temperature = (overrides?.temperature as number) ?? 0.7;
|
|
676
|
+
const maxTokens = (overrides?.maxTokens as number) ?? 4096;
|
|
677
|
+
const maxRetries = (this.config.llm as any)?.maxRetries ?? 2;
|
|
678
|
+
const isAnthropic = model.includes("claude") || model.startsWith("anthropic/");
|
|
679
|
+
|
|
680
|
+
let lastError: Error | null = null;
|
|
681
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
682
|
+
try {
|
|
683
|
+
if (attempt > 0) await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt - 1)));
|
|
684
|
+
|
|
685
|
+
let content: string;
|
|
686
|
+
let toolCalls: ToolCall[] = [];
|
|
687
|
+
let usage: UsageStats = { promptTokens: 0, completionTokens: 0 };
|
|
688
|
+
|
|
689
|
+
if (isAnthropic) {
|
|
690
|
+
const r = await this.callAnthropic(model, messages, tools, temperature, maxTokens);
|
|
691
|
+
content = r.content; toolCalls = r.toolCalls; usage = r.usage;
|
|
692
|
+
} else {
|
|
693
|
+
const r = await this.callOpenAI(model, messages, tools, temperature, maxTokens);
|
|
694
|
+
content = r.content; toolCalls = r.toolCalls; usage = r.usage;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const name = agentName || "default";
|
|
698
|
+
if (!this.usageStats.has(name)) this.usageStats.set(name, { prompt_tokens: 0, completion_tokens: 0, calls: 0, cost: 0 });
|
|
699
|
+
const s = this.usageStats.get(name)!;
|
|
700
|
+
s.prompt_tokens += usage.promptTokens; s.completion_tokens += usage.completionTokens; s.calls += 1;
|
|
701
|
+
const cost = estimateCost(model, usage.promptTokens, usage.completionTokens);
|
|
702
|
+
s.cost += cost; this.totalCost += cost;
|
|
703
|
+
|
|
704
|
+
return { content, toolCalls, model, usage, cost, truncated: false };
|
|
705
|
+
} catch (e: any) {
|
|
706
|
+
lastError = e;
|
|
707
|
+
if (attempt >= maxRetries) throw e;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
throw lastError || new Error("Unknown error");
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
private async callOpenAI(
|
|
714
|
+
m: string, messages: Record<string, unknown>[], tools?: string[], temp?: number, maxTok?: number
|
|
715
|
+
): Promise<{ content: string; toolCalls: ToolCall[]; usage: UsageStats }> {
|
|
716
|
+
const apiKey = this.getApiKey(m);
|
|
717
|
+
const baseUrl = this.getBaseUrl(m);
|
|
718
|
+
const body: Record<string, unknown> = { model: m, messages, temperature: temp ?? 0.7, max_tokens: maxTok ?? 4096 };
|
|
719
|
+
if (tools?.length) {
|
|
720
|
+
const defs = tools.map(t => this._toolRegistry.get(t)).filter(Boolean) as any[];
|
|
721
|
+
if (defs.length) body.tools = defs.map(t => ({ type: "function", function: { name: t.name, description: t.description, parameters: this.paramsToSchema(t.parameters || []) } }));
|
|
722
|
+
}
|
|
723
|
+
const resp = await fetch(baseUrl + "/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", Authorization: "Bearer " + apiKey }, body: JSON.stringify(body) });
|
|
724
|
+
if (!resp.ok) { const e: any = new Error("API " + resp.status + ": " + ((await resp.text()).slice(0, 200))); e.status_code = resp.status; throw e; }
|
|
725
|
+
const data: any = await resp.json();
|
|
726
|
+
const msg = data.choices?.[0]?.message || {};
|
|
727
|
+
return { content: msg.content || "", toolCalls: (msg.tool_calls || []).map((tc: any) => ({ id: tc.id, type: "function", function: { name: tc.function?.name || "", arguments: tc.function?.arguments || "{}" } })), usage: { promptTokens: data.usage?.prompt_tokens || 0, completionTokens: data.usage?.completion_tokens || 0 } };
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
private async callAnthropic(
|
|
731
|
+
m: string, messages: Record<string, unknown>[], tools?: string[], temp?: number, maxTok?: number
|
|
732
|
+
): Promise<{ content: string; toolCalls: ToolCall[]; usage: UsageStats }> {
|
|
733
|
+
const apiKey = this.getApiKey("anthropic");
|
|
734
|
+
const body: Record<string, unknown> = { model: m, max_tokens: maxTok ?? 4096, messages: messages.filter(msg => msg.role !== "system"), temperature: temp ?? 0.7 };
|
|
735
|
+
const sys = messages.find(msg => msg.role === "system"); if (sys) body.system = sys.content;
|
|
736
|
+
if (tools?.length) {
|
|
737
|
+
const defs = tools.map(t => this._toolRegistry.get(t)).filter(Boolean) as any[];
|
|
738
|
+
if (defs.length) body.tools = defs.map(t => ({ name: t.name, description: t.description, input_schema: this.paramsToSchema(t.parameters || []) }));
|
|
739
|
+
}
|
|
740
|
+
const resp = await fetch("https://api.anthropic.com/v1/messages", { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": apiKey, "anthropic-version": "2023-06-01" }, body: JSON.stringify(body) });
|
|
741
|
+
if (!resp.ok) { const e: any = new Error("API " + resp.status + ": " + ((await resp.text()).slice(0, 200))); e.status_code = resp.status; throw e; }
|
|
742
|
+
const data: any = await resp.json(); let content = ""; const toolCalls: ToolCall[] = [];
|
|
743
|
+
for (const b of data.content || []) { if (b.type === "text") content += b.text; if (b.type === "tool_use") toolCalls.push({ id: b.id, type: "function", function: { name: b.name, arguments: JSON.stringify(b.input) } }); }
|
|
744
|
+
return { content, toolCalls, usage: { promptTokens: data.usage?.input_tokens || 0, completionTokens: data.usage?.output_tokens || 0 } };
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
private paramsToSchema(params: any[]): Record<string, any> {
|
|
748
|
+
const props: Record<string, any> = {};
|
|
749
|
+
for (const p of params) props[p.name] = { type: p.type === "integer" ? "integer" : p.type === "number" ? "number" : p.type === "boolean" ? "boolean" : "string", description: p.description };
|
|
750
|
+
const required = params.filter(p => p.required).map(p => p.name);
|
|
751
|
+
return { type: "object", properties: props, ...(required.length > 0 ? { required } : {}) };
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
private getApiKey(model: string): string {
|
|
755
|
+
let provider = "openai"; const [pr] = splitProvider(model); if (pr) provider = pr;
|
|
756
|
+
else { const l = model.toLowerCase(); if (l.includes("claude")) provider = "anthropic"; else if (l.includes("deepseek")) provider = "deepseek"; else if (l.includes("groq")) provider = "groq"; else if (l.includes("openrouter")) provider = "openrouter"; else if (l.includes("gemini")) provider = "gemini"; }
|
|
757
|
+
const envMap = getProviderEnvMap();
|
|
758
|
+
const envVar = envMap.get(provider) || (provider.toUpperCase() + "_API_KEY");
|
|
759
|
+
const key = process.env[envVar];
|
|
760
|
+
if (!key) throw new Error("Missing " + envVar + ". Set environment variable or configure in ~/.skyloom/config.yaml");
|
|
761
|
+
return key;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
private getBaseUrl(model: string): string {
|
|
765
|
+
let provider = "openai"; const [pr] = splitProvider(model); if (pr) provider = pr;
|
|
766
|
+
else { const l = model.toLowerCase(); if (l.includes("claude")) return "https://api.anthropic.com/v1"; else if (l.includes("deepseek")) return "https://api.deepseek.com/v1"; else if (l.includes("groq")) return "https://api.groq.com/openai/v1"; else if (l.includes("openrouter")) return "https://openrouter.ai/api/v1"; else if (l.includes("ollama")) return ((process.env.OLLAMA_HOST || "http://localhost:11434") + "/v1"); }
|
|
767
|
+
if (provider === "deepseek") return "https://api.deepseek.com/v1";
|
|
768
|
+
if (provider === "groq") return "https://api.groq.com/openai/v1";
|
|
769
|
+
if (provider === "openrouter") return "https://openrouter.ai/api/v1";
|
|
770
|
+
if (provider === "ollama") return ((process.env.OLLAMA_HOST || "http://localhost:11434") + "/v1");
|
|
771
|
+
return "https://api.openai.com/v1";
|
|
695
772
|
}
|
|
696
773
|
|
|
697
|
-
/**
|
|
698
|
-
* Stream a completion (placeholder).
|
|
699
|
-
*/
|
|
700
774
|
async *stream(
|
|
701
|
-
|
|
702
|
-
_agentName?: string
|
|
775
|
+
messages: Record<string, unknown>[], agentName?: string
|
|
703
776
|
): AsyncGenerator<string> {
|
|
704
|
-
|
|
705
|
-
yield
|
|
777
|
+
const response = await this.complete(messages, agentName);
|
|
778
|
+
yield response.content;
|
|
706
779
|
}
|
|
707
780
|
|
|
708
|
-
/**
|
|
709
|
-
* Stream completion with tool awareness (placeholder).
|
|
710
|
-
*/
|
|
711
781
|
async *streamWithTools(
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
_tools?: string[],
|
|
715
|
-
_toolRegistry?: ToolRegistry,
|
|
716
|
-
_overrides?: Record<string, unknown>
|
|
782
|
+
messages: Record<string, unknown>[], agentName?: string, tools?: string[],
|
|
783
|
+
_toolRegistry?: ToolRegistry, overrides?: Record<string, unknown>
|
|
717
784
|
): AsyncGenerator<StreamEvent> {
|
|
718
|
-
|
|
719
|
-
yield {
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
};
|
|
785
|
+
const response = await this.complete(messages, agentName, tools, false, overrides);
|
|
786
|
+
if (response.content) yield { type: "content", text: response.content };
|
|
787
|
+
for (const tc of response.toolCalls || []) yield { type: "tool_call", toolCall: tc };
|
|
788
|
+
yield { type: "done", usage: response.usage, reasoningContent: response.reasoningContent };
|
|
723
789
|
}
|
|
724
790
|
}
|