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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skyloom",
3
- "version": "1.4.5",
3
+ "version": "1.5.0",
4
4
  "description": "天空织机 Skyloom — 6 weather-themed AI agents: Fog, Rain, Frost, Snow, Dew, Fair",
5
5
  "preferGlobal": true,
6
6
  "type": "commonjs",
package/src/core/llm.ts CHANGED
@@ -662,63 +662,129 @@ export class LLMClient {
662
662
  }
663
663
 
664
664
  /**
665
- * Complete with retry logic (placeholder).
665
+ * Complete with retry logic — real HTTP call to LLM API.
666
666
  */
667
667
  private async completeWithRetry(
668
668
  model: string,
669
- _messages: Record<string, unknown>[],
670
- _agentName?: string,
671
- _tools?: string[],
672
- _stream: boolean = false,
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
- // This is a placeholder. Real implementation would:
676
- // 1. Validate cache
677
- // 2. Call actual LLM API (OpenAI, Anthropic, etc.)
678
- // 3. Apply Anthropic cache control if needed
679
- // 4. Handle retry logic with exponential backoff
680
- // 5. Track usage and cost
681
- // 6. Cache results if appropriate
682
-
683
- const _temperature = (overrides?.temperature as number) ?? 0.7;
684
- const _maxTokens = (overrides?.maxTokens as number) ?? 2000;
685
-
686
- // For now, return a dummy response
687
- return {
688
- content: "Placeholder response from LLM",
689
- toolCalls: [],
690
- model,
691
- usage: { promptTokens: 100, completionTokens: 50 },
692
- cost: estimateCost(model, 100, 50),
693
- truncated: false,
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
- _messages: Record<string, unknown>[],
702
- _agentName?: string
775
+ messages: Record<string, unknown>[], agentName?: string
703
776
  ): AsyncGenerator<string> {
704
- // Placeholder implementation
705
- yield "Streaming response...";
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
- _messages: Record<string, unknown>[],
713
- _agentName?: string,
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
- // Placeholder implementation
719
- yield {
720
- type: "content",
721
- text: "Tool-aware streaming response...",
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
  }