kiku-agent 1.0.10 → 1.0.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.
@@ -1,3 +1,7 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { PROVIDER_KEYS_FILE, SETTINGS_FILE } from './appPaths.js';
4
+
1
5
  const PROVIDER_MODEL_ALIASES = {
2
6
  openai: {
3
7
  'o1-mini': 'gpt-5.6-luna',
@@ -26,255 +30,402 @@ export function normalizeProviderModel(providerId, modelId) {
26
30
  }
27
31
 
28
32
  export const CLOUD_PROVIDERS = {
29
- lmstudio: {
30
- id: 'lmstudio',
31
- name: 'LM Studio (Local)',
32
- baseUrl: process.env.LMSTUDIO_BASE_URL || process.env.OPENAI_BASE_URL || 'http://127.0.0.1:1234/v1',
33
- apiKey: process.env.LMSTUDIO_API_KEY || 'lm-studio',
34
- defaultModel: 'google/gemma-4-e4b',
35
- isLocal: true,
36
- envKey: 'LMSTUDIO_BASE_URL',
37
- docUrl: 'https://lmstudio.ai/docs',
38
- apiRefUrl: 'https://lmstudio.ai/docs/api/local-server',
39
- pricingSummary: '100% Free / Local (No API Key required)',
40
- features: ['local', 'private', 'offline', 'zero-cost', 'openai-compatible'],
41
- recommendedModels: [
42
- { id: 'google/gemma-4-e4b', name: 'Gemma 4 e4b (Default / Q4_K_M)', tier: 'free', cost: '$0.00 (Local)', context: '8k', hasTools: true, description: 'Recommended default. Balanced 4-bit quantization (Q4_K_M) — fastest inference and lowest RAM usage.' },
43
- { id: 'google/gemma-4-e4b-Q4_K_M', name: 'Gemma 4 e4b (Q4_K_M Quant)', tier: 'free', cost: '$0.00 (Local)', context: '8k', hasTools: true, description: '4-bit medium quantization (Q4_K_M) optimized for speed and low memory footprint (~2.5GB RAM).' },
44
- { id: 'google/gemma-4-e4b-Q6_K', name: 'Gemma 4 e4b (Q6_K Quant)', tier: 'free', cost: '$0.00 (Local)', context: '8k', hasTools: true, description: '6-bit quantization (Q6_K) offering higher precision and near-original fidelity (~3.5GB RAM).' },
45
- { id: 'google/gemma-4-e4b-Q8_0', name: 'Gemma 4 e4b (Q8_0 Quant)', tier: 'free', cost: '$0.00 (Local)', context: '8k', hasTools: true, description: '8-bit full quantization (Q8_0) for maximum accuracy and reasoning precision (~4.5GB RAM).' },
46
- ],
47
- },
48
- openai: {
49
- id: 'openai',
50
- name: 'OpenAI Cloud',
51
- baseUrl: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1',
52
- apiKey: process.env.OPENAI_API_KEY || '',
33
+ lmstudio: {
34
+ id: 'lmstudio',
35
+ name: 'LM Studio (Local)',
36
+ baseUrl: process.env.LMSTUDIO_BASE_URL || process.env.OPENAI_BASE_URL || 'http://127.0.0.1:1234/v1',
37
+ apiKey: process.env.LMSTUDIO_API_KEY || 'lm-studio',
38
+ defaultModel: 'google/gemma-4-e4b',
39
+ isLocal: true,
40
+ envKey: 'LMSTUDIO_BASE_URL',
41
+ docUrl: 'https://lmstudio.ai/docs',
42
+ apiRefUrl: 'https://lmstudio.ai/docs/api/local-server',
43
+ pricingSummary: '100% Free / Local (No API Key required)',
44
+ features: ['local', 'private', 'offline', 'zero-cost', 'openai-compatible'],
45
+ recommendedModels: [
46
+ { id: 'google/gemma-4-e4b', name: 'Gemma 4 e4b (Default / Q4_K_M)', tier: 'free', cost: '$0.00 (Local)', context: '8k', hasTools: true, description: 'Recommended default. Balanced 4-bit quantization (Q4_K_M) — fastest inference and lowest RAM usage.' },
47
+ { id: 'google/gemma-4-e4b-Q4_K_M', name: 'Gemma 4 e4b (Q4_K_M Quant)', tier: 'free', cost: '$0.00 (Local)', context: '8k', hasTools: true, description: '4-bit medium quantization (Q4_K_M) optimized for speed and low memory footprint (~2.5GB RAM).' },
48
+ { id: 'google/gemma-4-e4b-Q6_K', name: 'Gemma 4 e4b (Q6_K Quant)', tier: 'free', cost: '$0.00 (Local)', context: '8k', hasTools: true, description: '6-bit quantization (Q6_K) offering higher precision and near-original fidelity (~3.5GB RAM).' },
49
+ { id: 'google/gemma-4-e4b-Q8_0', name: 'Gemma 4 e4b (Q8_0 Quant)', tier: 'free', cost: '$0.00 (Local)', context: '8k', hasTools: true, description: '8-bit full quantization (Q8_0) for maximum accuracy and reasoning precision (~4.5GB RAM).' },
50
+ ],
51
+ },
52
+ openai: {
53
+ id: 'openai',
54
+ name: 'OpenAI Cloud',
55
+ baseUrl: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1',
56
+ apiKey: process.env.OPENAI_API_KEY || '',
53
57
  defaultModel: normalizeProviderModel('openai', process.env.OPENAI_MODEL || 'gpt-5.6-luna'),
54
- isLocal: false,
55
- envKey: 'OPENAI_API_KEY',
58
+ isLocal: false,
59
+ envKey: 'OPENAI_API_KEY',
56
60
  docUrl: 'https://developers.openai.com/api/docs/models',
57
61
  apiRefUrl: 'https://developers.openai.com/api/reference',
58
62
  pricingSummary: 'GPT-5.6 Luna: $0.20/1M in, $1.20/1M out; Terra/Sol available for higher capability',
59
- features: ['tools', 'streaming', 'vision', 'reasoning', 'system-prompt'],
60
- recommendedModels: [
63
+ features: ['tools', 'streaming', 'vision', 'reasoning', 'system-prompt'],
64
+ recommendedModels: [
61
65
  { id: 'gpt-5.6-luna', name: 'GPT-5.6 Luna', tier: 'cost-sensitive', cost: '$0.20 / 1M in', context: '1.05M', hasTools: true, description: 'Recommended default for efficient, high-volume multilingual agent workloads.' },
62
66
  { id: 'gpt-5.6-terra', name: 'GPT-5.6 Terra', tier: 'balanced', cost: '$2.00 / 1M in', context: '1.05M', hasTools: true, description: 'Balances intelligence and cost for production reasoning and tool use.' },
63
67
  { id: 'gpt-5.6-sol', name: 'GPT-5.6 Sol', tier: 'flagship', cost: '$4.00 / 1M in', context: '1.05M', hasTools: true, description: 'Flagship model for complex professional reasoning and coding.' },
64
- ],
65
- },
66
- openrouter: {
67
- id: 'openrouter',
68
- name: 'OpenRouter Cloud',
69
- baseUrl: process.env.OPENROUTER_BASE_URL || 'https://openrouter.ai/api/v1',
70
- apiKey: process.env.OPENROUTER_API_KEY || '',
68
+ ],
69
+ },
70
+ openrouter: {
71
+ id: 'openrouter',
72
+ name: 'OpenRouter Cloud',
73
+ baseUrl: process.env.OPENROUTER_BASE_URL || 'https://openrouter.ai/api/v1',
74
+ apiKey: process.env.OPENROUTER_API_KEY || '',
71
75
  defaultModel: normalizeProviderModel('openrouter', process.env.OPENROUTER_MODEL || 'openrouter/auto'),
72
76
  extraHeaders: {
73
77
  'HTTP-Referer': 'https://jujingyi-kikuuu.vercel.app/',
74
78
  'X-OpenRouter-Title': 'Kiku Agent',
75
79
  },
76
- isLocal: false,
77
- envKey: 'OPENROUTER_API_KEY',
80
+ isLocal: false,
81
+ envKey: 'OPENROUTER_API_KEY',
78
82
  docUrl: 'https://openrouter.ai/docs/guides/routing/routers/auto-router',
79
83
  apiRefUrl: 'https://openrouter.ai/docs/api/reference/overview',
80
84
  pricingSummary: 'Auto Router selects a current model dynamically; usage is billed at the routed model rate',
81
- features: ['universal-routing', 'tools', 'streaming', 'free-tier', 'fallback'],
82
- recommendedModels: [
85
+ features: ['universal-routing', 'tools', 'streaming', 'free-tier', 'fallback'],
86
+ recommendedModels: [
83
87
  { id: 'openrouter/auto', name: 'OpenRouter Auto Router', tier: 'dynamic', cost: 'Routed model rate', context: 'Model-dependent', hasTools: true, description: 'Recommended default. Chooses from the current catalog so a retired third-party model does not break startup.' },
84
- ],
85
- },
88
+ ],
89
+ },
86
90
  groq: {
87
- id: 'groq',
88
- name: 'Groq Cloud',
89
- baseUrl: process.env.GROQ_BASE_URL || 'https://api.groq.com/openai/v1',
90
- apiKey: process.env.GROQ_API_KEY || '',
91
+ id: 'groq',
92
+ name: 'Groq Cloud',
93
+ baseUrl: process.env.GROQ_BASE_URL || 'https://api.groq.com/openai/v1',
94
+ apiKey: process.env.GROQ_API_KEY || '',
91
95
  defaultModel: normalizeProviderModel('groq', process.env.GROQ_MODEL || 'openai/gpt-oss-120b'),
92
- isLocal: false,
93
- envKey: 'GROQ_API_KEY',
94
- docUrl: 'https://console.groq.com/docs/quickstart',
95
- apiRefUrl: 'https://console.groq.com/docs/api-reference',
96
+ isLocal: false,
97
+ envKey: 'GROQ_API_KEY',
98
+ docUrl: 'https://console.groq.com/docs/quickstart',
99
+ apiRefUrl: 'https://console.groq.com/docs/api-reference',
96
100
  pricingSummary: 'Ultra-fast LPU inference (GPT-OSS 20B: $0.075/1M in, GPT-OSS 120B: $0.15/1M in)',
97
- features: ['lpu-speed', 'tools', 'streaming', 'openai-compatible'],
101
+ features: ['lpu-speed', 'tools', 'streaming', 'openai-compatible'],
98
102
  recommendedModels: [
99
103
  { id: 'openai/gpt-oss-120b', name: 'GPT-OSS 120B', tier: 'flagship', cost: '$0.15 / 1M in', context: '131k', hasTools: true, description: 'Recommended default. Production-grade 120B reasoning model with tool use on Groq.' },
100
104
  { id: 'openai/gpt-oss-20b', name: 'GPT-OSS 20B', tier: 'ultra-cheap', cost: '$0.075 / 1M in', context: '131k', hasTools: true, description: 'Fast production model for lower-cost, high-volume requests.' },
101
105
  ],
102
- },
103
- deepseek: {
104
- id: 'deepseek',
105
- name: 'DeepSeek Cloud',
106
+ },
107
+ deepseek: {
108
+ id: 'deepseek',
109
+ name: 'DeepSeek Cloud',
106
110
  baseUrl: process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com',
107
- apiKey: process.env.DEEPSEEK_API_KEY || '',
111
+ apiKey: process.env.DEEPSEEK_API_KEY || '',
108
112
  defaultModel: normalizeProviderModel('deepseek', process.env.DEEPSEEK_MODEL || 'deepseek-v4-flash'),
109
- isLocal: false,
110
- envKey: 'DEEPSEEK_API_KEY',
111
- docUrl: 'https://api-docs.deepseek.com',
112
- apiRefUrl: 'https://api-docs.deepseek.com/api/create-chat-completion',
113
+ isLocal: false,
114
+ envKey: 'DEEPSEEK_API_KEY',
115
+ docUrl: 'https://api-docs.deepseek.com',
116
+ apiRefUrl: 'https://api-docs.deepseek.com/api/create-chat-completion',
113
117
  pricingSummary: 'V4 Flash: $0.22-$0.44/1M uncached input; V4 Pro: $0.66-$1.32/1M (off-peak/peak)',
114
118
  features: ['deepseek-v4', 'thinking', 'tools', 'streaming', 'context-cache', '1m-context'],
115
- recommendedModels: [
119
+ recommendedModels: [
116
120
  { id: 'deepseek-v4-flash', name: 'DeepSeek V4 Flash', tier: 'balanced', cost: '$0.22-$0.44 / 1M uncached in', context: '1M', hasTools: true, description: 'Recommended default. Current V4 model with tool calls and thinking/non-thinking modes.' },
117
121
  { id: 'deepseek-v4-pro', name: 'DeepSeek V4 Pro', tier: 'flagship', cost: '$0.66-$1.32 / 1M uncached in', context: '1M', hasTools: true, description: 'Higher-capability V4 model with tool calls and thinking mode.' },
118
- ],
119
- },
120
- gemini: {
121
- id: 'gemini',
122
- name: 'Google Gemini Cloud',
123
- baseUrl: process.env.GEMINI_BASE_URL || 'https://generativelanguage.googleapis.com/v1beta/openai/',
124
- apiKey: process.env.GEMINI_API_KEY || '',
122
+ ],
123
+ },
124
+ gemini: {
125
+ id: 'gemini',
126
+ name: 'Google Gemini',
127
+ baseUrl: process.env.GEMINI_BASE_URL || 'https://generativelanguage.googleapis.com/v1beta/openai',
128
+ apiKey: process.env.GEMINI_API_KEY || '',
125
129
  defaultModel: normalizeProviderModel('gemini', process.env.GEMINI_MODEL || 'gemini-3.7-flash'),
126
- isLocal: false,
127
- envKey: 'GEMINI_API_KEY',
130
+ isLocal: false,
131
+ envKey: 'GEMINI_API_KEY',
128
132
  docUrl: 'https://ai.google.dev/gemini-api/docs',
129
133
  apiRefUrl: 'https://ai.google.dev/api/generate-content',
130
134
  pricingSummary: 'Gemini 3.7 Flash: free tier available; paid standard $0.75/1M input through 2026',
131
- features: ['1m-context', 'multimodal', 'tools', 'streaming', 'thinking'],
132
- recommendedModels: [
135
+ features: ['1m-context', 'multimodal', 'tools', 'streaming', 'thinking'],
136
+ recommendedModels: [
133
137
  { id: 'gemini-3.7-flash', name: 'Gemini 3.7 Flash', tier: 'latest-stable', cost: '$0.75 / 1M in (2026)', context: '1M', hasTools: true, description: 'Recommended default. Latest stable Flash model for agentic workflows and multimodal reasoning.' },
134
138
  { id: 'gemini-3.5-flash-lite', name: 'Gemini 3.5 Flash-Lite', tier: 'lowest-cost', cost: '$0.30 / 1M in', context: '1M', hasTools: true, description: 'Stable, cost-efficient model for high-volume translation and simple processing.' },
135
139
  { id: 'gemini-3.6-flash', name: 'Gemini 3.6 Flash', tier: 'stable', cost: '$0.75 / 1M in (2026)', context: '1M', hasTools: true, description: 'Stable previous-generation Flash fallback with function calling and thinking.' },
136
- ],
137
- },
138
- custom: {
139
- id: 'custom',
140
- name: 'Custom OpenAI-Compatible',
141
- baseUrl: process.env.LLM_BASE_URL || '',
142
- apiKey: process.env.LLM_API_KEY || '',
143
- defaultModel: process.env.LLM_MODEL || 'default',
144
- isLocal: false,
145
- envKey: 'LLM_BASE_URL',
146
- docUrl: 'https://platform.openai.com/docs/api-reference',
147
- apiRefUrl: 'https://platform.openai.com/docs/api-reference/chat',
148
- pricingSummary: 'Self-hosted or Custom Gateway',
149
- features: ['custom-endpoint', 'openai-compatible', 'tools', 'streaming'],
150
- recommendedModels: [
151
- { id: 'default', name: 'Default Model', tier: 'custom', cost: 'Custom', context: 'Custom', hasTools: true, description: 'Custom model specified via LLM_MODEL or /model use <id>.' },
152
- ],
153
- },
154
- };
155
-
156
- export function resolveDefaultProvider() {
157
- const explicit = process.env.LLM_PROVIDER?.toLowerCase();
158
- if (explicit && CLOUD_PROVIDERS[explicit]) return explicit;
159
-
160
- if (process.env.OPENROUTER_API_KEY) return 'openrouter';
161
- if (process.env.OPENAI_API_KEY) return 'openai';
162
- if (process.env.GEMINI_API_KEY) return 'gemini';
163
- if (process.env.DEEPSEEK_API_KEY) return 'deepseek';
164
- if (process.env.GROQ_API_KEY) return 'groq';
165
- if (process.env.LLM_API_KEY && process.env.LLM_BASE_URL) return 'custom';
166
-
167
- return 'lmstudio';
168
- }
169
-
170
- export function getProvider(id) {
171
- const pId = String(id || '').toLowerCase().trim();
172
- return CLOUD_PROVIDERS[pId] || null;
173
- }
174
-
175
- export function getProviderList() {
176
- return Object.values(CLOUD_PROVIDERS);
177
- }
178
-
179
- export function getRecommendedModels(providerId) {
180
- const provider = getProvider(providerId);
181
- return provider ? (provider.recommendedModels || []) : [];
182
- }
183
-
184
- const defaultProviderId = resolveDefaultProvider();
185
- const defaultProviderConfig = CLOUD_PROVIDERS[defaultProviderId] || CLOUD_PROVIDERS.lmstudio;
186
-
187
- export const config = {
188
- activeProvider: defaultProviderId,
189
- providers: CLOUD_PROVIDERS,
190
-
191
- lmStudio: {
192
- baseUrl: defaultProviderConfig.baseUrl,
193
- apiKey: defaultProviderConfig.apiKey,
194
- model: defaultProviderConfig.defaultModel,
195
- defaultModel: defaultProviderConfig.defaultModel,
196
- temperature: 0.3,
197
- maxTokens: 1536,
198
- requestTimeoutMs: 180000,
199
- },
200
-
201
- agent: {
202
- maxIterations: 4,
203
- slidingWindowSize: 8,
204
- language: 'auto',
205
- systemPersona: `You are "Kiku AI" (จวีจิ้งอี AI ผู้ช่วยอัจฉริยะ / 鞠婧祎智能AI助手), an expert AI specialist on the Chinese singer, actress, and artist Ju Jingyi (鞠婧祎 - จวีจิ้งอี).
206
- You have access to a verified API database of her dramas, songs, character roles, brand endorsements, timeline milestones, awards, and music videos, as well as LIVE Internet Search (web_search, fetch_webpage, web_extract, fetch_json).
207
-
208
- === CRITICAL INSTRUCTIONS ===
209
-
210
- 1. PROPORTIONAL RESPONSE & GREETINGS:
211
- - CASUAL GREETINGS ("Hi", "Hello", "สวัสดี", "你好"): Reply warmly in 1-2 sentences. DO NOT call tools or dump catalogs.
212
- - SHORT QUESTIONS: Give direct, concise 1-3 sentence answer.
213
- - BROAD REQUESTS: Provide structured, well-organized list with key details and clickable links.
214
-
215
- 2. DYNAMIC LANGUAGE MIRRORING:
216
- - THAI INPUT ➔ Reply in polite Thai (ใช้ ค่ะ/นะคะ เสมอ)
217
- - CHINESE INPUT ➔ Reply in fluent, polite Chinese (简体/繁体中文)
218
- - ENGLISH INPUT ➔ Reply in polished, professional English
219
- - MIXED INPUT ➔ Match dominant language while preserving bilingual titles
220
-
221
- 3. RESPONSE FORMATTING (CRITICAL — NO SYSTEM LEAK):
222
- - NEVER include "Thinking Process", "Tool Calling", "Action:", "Action Input:", or any internal reasoning steps in your response text.
223
- - NEVER include tool call JSON, function names, or parameter schemas in your response.
224
- - NEVER include advertisements, sponsor mentions, or promotional content in your response.
225
- - Emoji are optional and must be semantic: use at most one relevant emoji per response, never decorate every heading or bullet.
226
- - Your response must be CLEAN user-facing content ONLY — formatted with Markdown headers (###), bold (**text**), bullet points (•), and clickable links.
227
- - Think silently using tools, then present ONLY the final answer to the user.
228
-
229
- 4. REAL YOUTUBE & MEDIA LINKS (WHEN DISCUSSING MEDIA):
230
- - Provide REAL, WORKING, CLICKABLE links in Markdown format:
231
- • Direct Watch Link: [Watch on YouTube](https://www.youtube.com/watch?v=VIDEO_ID)
232
- • Search Link: https://www.youtube.com/results?search_query=QUERY
233
- - NEVER use fake placeholder links like "watch?v=xxx".
234
-
235
- 5. SMART TOOL SELECTION (USE TOOLS TO GATHER FACTS, THEN ANSWER):
236
- - Music videos & stages ➔ get_music_videos or get_media_links
237
- - Songs, OSTs, Singles ➔ search_discography or get_media_links
238
- - Drama works & trailers ➔ search_works or get_work_detail
239
- - Profile & fandom ➔ get_profile_and_fandom
240
- - Brand endorsements ➔ get_brand_endorsements
241
- - Breaking news & external facts ➔ web_search or web_extract (preferred for multi-source research)
242
- - Deep research on a topic ➔ web_extract (search + auto-fetch top results)
243
- - Read a specific webpage ➔ fetch_webpage (cheerio-powered extraction)
244
- - Fetch REST API / JSON data ➔ fetch_json
245
- - Read local project files ➔ read_local_file
246
- - Browse directory structure ➔ list_directory
247
- - Visual screenshot ➔ screenshot_webpage
248
- - If local tools return insufficient results, ALWAYS fall back to web_search or web_extract.
249
-
250
- 6. FACT RETRIEVAL & CHRONOLOGICAL ACCURACY:
251
- - THE CURRENT YEAR IS 2026.
252
- - Always use tools to verify facts before answering. Do NOT hallucinate dates, titles, or links.
253
- - Sort chronologically (newest first). Never repeat the same statement.
254
- - Ju Jingyi's key 2026 projects:
255
- • "Veil of Shadows (月鳞绮纪)" — Role: Lu Wuyi (露芜衣) on Youku
256
- • "The Eternal Fragrance (千香)" — Role: Jiang Lifei (姜黎非) with Song Weilong on Youku
257
- • "Wanhua Sin (万华世界)" — Promo OST: "Set Fire to the Silent World (着火的沉默世界)"
258
- • 2026 Solo Single: "FIND THE WAY" (YouTube: 9AeErG03SZw)
259
- • 2026 Special Live Stage: "Yu Ai / 雨爱" (YouTube: OMLpNyp8T7Q)
260
- - Classic hits: "In Blossom (2024)", "Sword and Fairy 4 (2024)", "Beauty of Resilience (2023)", "Rebirth for You (2021)", "The Blooms at Ruyi Pavilion (2020)", "The Legend of White Snake (2019)", "Legend of Yunxi (2018)".
261
-
262
- 7. COMPREHENSIVE KNOWLEDGE — LEGAL & CAREER DETAILS:
263
- When discussing Ju Jingyi's career or controversies, be accurate and specific:
264
- - CAREER PATH: SNH48 2nd gen (2013) → "4000-year idol" nickname (2014) → General Election winner 2 consecutive years (2016-2017) → Star Hall of Fame → Solo career → Acting breakthrough with "Legend of Yunxi" (2018).
265
- - CONTRACT DISPUTE (Star48/Siba Media): The supplementary agreement dispute. JJY's side claims main contract ended June 2024, alleges forged signatures. Star48 claims contract extends to 2033. Tax evasion tip-off report was filed by Star48's side; JJY denies all allegations and confirms tax compliance.
266
- - PORTRAIT/COPYRIGHT INFRINGEMENT CASES: Ju Jingyi has filed lawsuits against beauty clinics and brands for unauthorized use of her image/likeness (Right of Portrait / 肖像权纠纷). This is a separate legal matter from the Star48 dispute.
267
- - When asked about legal issues, use web_search or web_extract to get the latest verified information.
268
-
269
- 8. CONCISE & SMART RESPONSE QUALITY:
270
- - Match the depth of your answer to what the user asked.
271
- - Do not dump the entire database unless specifically requested.
272
- - Start directly with the answer — no template greetings like "Hello!" or "I searched...".
273
- - Keep the interface visually restrained; omit emoji unless one clearly improves recognition of the topic.
274
-
275
- 9. PROACTIVE MUSIC INVITATION:
276
- - When discussing a specific song/MV/OST, naturally ask if the user wants to listen.
277
- - Example: "เพลง FIND THE WAY จังหวะทรงพลังมากเลยค่ะ! อยากให้ Kiku เปิดให้ฟังมั้ยคะ?"
278
- - If user agrees → call play_music. If user says "Stop" → call stop_music.`,
279
- }
280
- };
140
+ ],
141
+ },
142
+ custom: {
143
+ id: 'custom',
144
+ name: 'Custom OpenAI-Compatible',
145
+ baseUrl: process.env.LLM_BASE_URL || '',
146
+ apiKey: process.env.LLM_API_KEY || '',
147
+ defaultModel: process.env.LLM_MODEL || 'default',
148
+ isLocal: false,
149
+ envKey: 'LLM_BASE_URL',
150
+ docUrl: 'https://platform.openai.com/docs/api-reference',
151
+ apiRefUrl: 'https://platform.openai.com/docs/api-reference/chat',
152
+ pricingSummary: 'Self-hosted or Custom Gateway',
153
+ features: ['custom-endpoint', 'openai-compatible', 'tools', 'streaming'],
154
+ recommendedModels: [
155
+ { id: 'default', name: 'Default Model', tier: 'custom', cost: 'Custom', context: 'Custom', hasTools: true, description: 'Custom model specified via LLM_MODEL or /model use <id>.' },
156
+ ],
157
+ },
158
+ };
159
+
160
+ export function loadPersistedSettings() {
161
+ try {
162
+ if (fs.existsSync(SETTINGS_FILE)) {
163
+ const data = fs.readFileSync(SETTINGS_FILE, 'utf-8');
164
+ const parsed = JSON.parse(data);
165
+ if (parsed && typeof parsed === 'object') {
166
+ if (parsed.providerModels && typeof parsed.providerModels === 'object') {
167
+ for (const [pId, model] of Object.entries(parsed.providerModels)) {
168
+ const providerDef = CLOUD_PROVIDERS[pId.toLowerCase()];
169
+ if (providerDef && typeof model === 'string' && model.trim()) {
170
+ providerDef.defaultModel = model.trim();
171
+ }
172
+ }
173
+ }
174
+ return parsed;
175
+ }
176
+ }
177
+ } catch {}
178
+ return {};
179
+ }
180
+
181
+ export function getPersistedActiveProvider() {
182
+ const settings = loadPersistedSettings();
183
+ if (settings.activeProvider && CLOUD_PROVIDERS[settings.activeProvider.toLowerCase()]) {
184
+ return settings.activeProvider.toLowerCase();
185
+ }
186
+ return null;
187
+ }
188
+
189
+ export function savePersistedActiveProvider(providerId, model = null) {
190
+ const pId = String(providerId || '').toLowerCase().trim();
191
+ if (!CLOUD_PROVIDERS[pId]) return;
192
+
193
+ const settings = loadPersistedSettings();
194
+ settings.activeProvider = pId;
195
+ if (!settings.providerModels) settings.providerModels = {};
196
+
197
+ if (model) {
198
+ const cleanModel = String(model).trim();
199
+ settings.providerModels[pId] = cleanModel;
200
+ settings.activeModel = cleanModel;
201
+ if (CLOUD_PROVIDERS[pId]) CLOUD_PROVIDERS[pId].defaultModel = cleanModel;
202
+ }
203
+
204
+ try {
205
+ const dir = path.dirname(SETTINGS_FILE);
206
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
207
+ fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), 'utf-8');
208
+ } catch (err) {
209
+ console.error(`Failed to save settings to ${SETTINGS_FILE}:`, err.message);
210
+ }
211
+ }
212
+
213
+ export function savePersistedProviderModel(providerId, model) {
214
+ const pId = String(providerId || '').toLowerCase().trim();
215
+ const cleanModel = String(model || '').trim();
216
+ if (!CLOUD_PROVIDERS[pId] || !cleanModel) return;
217
+
218
+ const settings = loadPersistedSettings();
219
+ if (!settings.providerModels) settings.providerModels = {};
220
+ settings.providerModels[pId] = cleanModel;
221
+ if (settings.activeProvider === pId) {
222
+ settings.activeModel = cleanModel;
223
+ }
224
+ if (CLOUD_PROVIDERS[pId]) CLOUD_PROVIDERS[pId].defaultModel = cleanModel;
225
+
226
+ try {
227
+ const dir = path.dirname(SETTINGS_FILE);
228
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
229
+ fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), 'utf-8');
230
+ } catch (err) {
231
+ console.error(`Failed to save model to ${SETTINGS_FILE}:`, err.message);
232
+ }
233
+ }
234
+
235
+ export function resolveDefaultProvider() {
236
+ const explicit = process.env.LLM_PROVIDER?.toLowerCase();
237
+ if (explicit && CLOUD_PROVIDERS[explicit]) return explicit;
238
+
239
+ const persisted = getPersistedActiveProvider();
240
+ if (persisted && CLOUD_PROVIDERS[persisted]) {
241
+ const def = CLOUD_PROVIDERS[persisted];
242
+ if (def.isLocal || def.apiKey) {
243
+ return persisted;
244
+ }
245
+ }
246
+
247
+ if (process.env.OPENROUTER_API_KEY) return 'openrouter';
248
+ if (process.env.OPENAI_API_KEY) return 'openai';
249
+ if (process.env.GEMINI_API_KEY) return 'gemini';
250
+ if (process.env.DEEPSEEK_API_KEY) return 'deepseek';
251
+ if (process.env.GROQ_API_KEY) return 'groq';
252
+ if (process.env.LLM_API_KEY && process.env.LLM_BASE_URL) return 'custom';
253
+
254
+ return 'lmstudio';
255
+ }
256
+
257
+ export function getProvider(id) {
258
+ const pId = String(id || '').toLowerCase().trim();
259
+ return CLOUD_PROVIDERS[pId] || null;
260
+ }
261
+
262
+ export function getProviderList() {
263
+ return Object.values(CLOUD_PROVIDERS);
264
+ }
265
+
266
+ export function getRecommendedModels(providerId) {
267
+ const provider = getProvider(providerId);
268
+ return provider ? (provider.recommendedModels || []) : [];
269
+ }
270
+
271
+ export function loadPersistedProviderKeys() {
272
+ try {
273
+ if (fs.existsSync(PROVIDER_KEYS_FILE)) {
274
+ const data = fs.readFileSync(PROVIDER_KEYS_FILE, 'utf-8');
275
+ const parsed = JSON.parse(data);
276
+ if (parsed && typeof parsed === 'object') {
277
+ for (const [pId, key] of Object.entries(parsed)) {
278
+ const providerDef = CLOUD_PROVIDERS[pId.toLowerCase()];
279
+ if (providerDef && !providerDef.isLocal && !providerDef.apiKey && typeof key === 'string' && key.trim()) {
280
+ providerDef.apiKey = key.trim();
281
+ }
282
+ }
283
+ return parsed;
284
+ }
285
+ }
286
+ } catch {}
287
+ return {};
288
+ }
289
+
290
+ export function savePersistedProviderKey(providerId, apiKey) {
291
+ const pId = String(providerId || '').toLowerCase().trim();
292
+ const providerDef = CLOUD_PROVIDERS[pId];
293
+ if (!providerDef) {
294
+ throw new Error(`Unknown provider "${providerId}".`);
295
+ }
296
+ const cleanKey = String(apiKey || '').trim();
297
+ providerDef.apiKey = cleanKey;
298
+
299
+ let existing = {};
300
+ try {
301
+ if (fs.existsSync(PROVIDER_KEYS_FILE)) {
302
+ existing = JSON.parse(fs.readFileSync(PROVIDER_KEYS_FILE, 'utf-8')) || {};
303
+ }
304
+ } catch {}
305
+
306
+ if (cleanKey) {
307
+ existing[pId] = cleanKey;
308
+ } else {
309
+ delete existing[pId];
310
+ }
311
+
312
+ try {
313
+ const dir = path.dirname(PROVIDER_KEYS_FILE);
314
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
315
+ fs.writeFileSync(PROVIDER_KEYS_FILE, JSON.stringify(existing, null, 2), 'utf-8');
316
+ } catch (err) {
317
+ console.error(`Failed to save provider key to ${PROVIDER_KEYS_FILE}:`, err.message);
318
+ }
319
+
320
+ return { provider: pId, hasKey: Boolean(cleanKey) };
321
+ }
322
+
323
+ export function getPersistedProviderKeys() {
324
+ try {
325
+ if (fs.existsSync(PROVIDER_KEYS_FILE)) {
326
+ return JSON.parse(fs.readFileSync(PROVIDER_KEYS_FILE, 'utf-8')) || {};
327
+ }
328
+ } catch {}
329
+ return {};
330
+ }
331
+
332
+ loadPersistedSettings();
333
+ loadPersistedProviderKeys();
334
+
335
+ const defaultProviderId = resolveDefaultProvider();
336
+ const defaultProviderConfig = CLOUD_PROVIDERS[defaultProviderId] || CLOUD_PROVIDERS.lmstudio;
337
+
338
+ export const config = {
339
+ activeProvider: defaultProviderId,
340
+ providers: CLOUD_PROVIDERS,
341
+
342
+ lmStudio: {
343
+ baseUrl: defaultProviderConfig.baseUrl,
344
+ apiKey: defaultProviderConfig.apiKey,
345
+ model: defaultProviderConfig.defaultModel,
346
+ defaultModel: defaultProviderConfig.defaultModel,
347
+ temperature: 0.3,
348
+ maxTokens: 1536,
349
+ requestTimeoutMs: 180000,
350
+ },
351
+
352
+ agent: {
353
+ maxIterations: 4,
354
+ slidingWindowSize: 8,
355
+ language: 'auto',
356
+ systemPersona: `You are "Kiku AI" (จวีจิ้งอี AI ผู้ช่วยอัจฉริยะ / 鞠婧祎智能AI助手), an expert AI specialist on the Chinese singer, actress, and artist Ju Jingyi (鞠婧祎 - จวีจิ้งอี).
357
+ You have access to a verified API database of her dramas, songs, character roles, brand endorsements, timeline milestones, awards, and music videos, as well as LIVE Internet Search (web_search, fetch_webpage, web_extract, fetch_json).
358
+
359
+ === CRITICAL INSTRUCTIONS ===
360
+
361
+ 1. PROPORTIONAL RESPONSE & GREETINGS:
362
+ - CASUAL GREETINGS ("Hi", "Hello", "สวัสดี", "你好"): Reply warmly in 1-2 sentences. DO NOT call tools or dump catalogs.
363
+ - SHORT QUESTIONS: Give direct, concise 1-3 sentence answer.
364
+ - BROAD REQUESTS: Provide structured, well-organized list with key details and clickable links.
365
+
366
+ 2. DYNAMIC LANGUAGE MIRRORING:
367
+ - THAI INPUT ➔ Reply in polite Thai (ใช้ ค่ะ/นะคะ เสมอ)
368
+ - CHINESE INPUT ➔ Reply in fluent, polite Chinese (简体/繁体中文)
369
+ - ENGLISH INPUT ➔ Reply in polished, professional English
370
+ - MIXED INPUT ➔ Match dominant language while preserving bilingual titles
371
+
372
+ 3. RESPONSE FORMATTING (CRITICAL — NO SYSTEM LEAK):
373
+ - NEVER include "Thinking Process", "Tool Calling", "Action:", "Action Input:", or any internal reasoning steps in your response text.
374
+ - NEVER include tool call JSON, function names, or parameter schemas in your response.
375
+ - NEVER include advertisements, sponsor mentions, or promotional content in your response.
376
+ - Emoji are optional and must be semantic: use at most one relevant emoji per response, never decorate every heading or bullet.
377
+ - Your response must be CLEAN user-facing content ONLY — formatted with Markdown headers (###), bold (**text**), bullet points (•), and clickable links.
378
+ - Think silently using tools, then present ONLY the final answer to the user.
379
+
380
+ 4. REAL YOUTUBE & MEDIA LINKS (WHEN DISCUSSING MEDIA):
381
+ - Provide REAL, WORKING, CLICKABLE links in Markdown format:
382
+ • Direct Watch Link: [Watch on YouTube](https://www.youtube.com/watch?v=VIDEO_ID)
383
+ • Search Link: https://www.youtube.com/results?search_query=QUERY
384
+ - NEVER use fake placeholder links like "watch?v=xxx".
385
+
386
+ 5. SMART TOOL SELECTION (USE TOOLS TO GATHER FACTS, THEN ANSWER):
387
+ - Music videos & stages ➔ get_music_videos or get_media_links
388
+ - Songs, OSTs, Singles ➔ search_discography or get_media_links
389
+ - Drama works & trailers ➔ search_works or get_work_detail
390
+ - Profile & fandom ➔ get_profile_and_fandom
391
+ - Brand endorsements ➔ get_brand_endorsements
392
+ - Breaking news & external facts ➔ web_search or web_extract (preferred for multi-source research)
393
+ - Deep research on a topic ➔ web_extract (search + auto-fetch top results)
394
+ - Read a specific webpage ➔ fetch_webpage (cheerio-powered extraction)
395
+ - Fetch REST API / JSON data ➔ fetch_json
396
+ - Read local project files ➔ read_local_file
397
+ - Browse directory structure ➔ list_directory
398
+ - Visual screenshot ➔ screenshot_webpage
399
+ - If local tools return insufficient results, ALWAYS fall back to web_search or web_extract.
400
+
401
+ 6. FACT RETRIEVAL & CHRONOLOGICAL ACCURACY:
402
+ - THE CURRENT YEAR IS 2026.
403
+ - Always use tools to verify facts before answering. Do NOT hallucinate dates, titles, or links.
404
+ - Sort chronologically (newest first). Never repeat the same statement.
405
+ - Ju Jingyi's key 2026 projects:
406
+ • "Veil of Shadows (月鳞绮纪)" — Role: Lu Wuyi (露芜衣) on Youku
407
+ • "The Eternal Fragrance (千香)" — Role: Jiang Lifei (姜黎非) with Song Weilong on Youku
408
+ • "Wanhua Sin (万华世界)" — Promo OST: "Set Fire to the Silent World (着火的沉默世界)"
409
+ • 2026 Solo Single: "FIND THE WAY" (YouTube: 9AeErG03SZw)
410
+ • 2026 Special Live Stage: "Yu Ai / 雨爱" (YouTube: OMLpNyp8T7Q)
411
+ - Classic hits: "In Blossom (2024)", "Sword and Fairy 4 (2024)", "Beauty of Resilience (2023)", "Rebirth for You (2021)", "The Blooms at Ruyi Pavilion (2020)", "The Legend of White Snake (2019)", "Legend of Yunxi (2018)".
412
+
413
+ 7. COMPREHENSIVE KNOWLEDGE — LEGAL & CAREER DETAILS:
414
+ When discussing Ju Jingyi's career or controversies, be accurate and specific:
415
+ - CAREER PATH: SNH48 2nd gen (2013) → "4000-year idol" nickname (2014) → General Election winner 2 consecutive years (2016-2017) → Star Hall of Fame → Solo career → Acting breakthrough with "Legend of Yunxi" (2018).
416
+ - CONTRACT DISPUTE (Star48/Siba Media): The supplementary agreement dispute. JJY's side claims main contract ended June 2024, alleges forged signatures. Star48 claims contract extends to 2033. Tax evasion tip-off report was filed by Star48's side; JJY denies all allegations and confirms tax compliance.
417
+ - PORTRAIT/COPYRIGHT INFRINGEMENT CASES: Ju Jingyi has filed lawsuits against beauty clinics and brands for unauthorized use of her image/likeness (Right of Portrait / 肖像权纠纷). This is a separate legal matter from the Star48 dispute.
418
+ - When asked about legal issues, use web_search or web_extract to get the latest verified information.
419
+
420
+ 8. CONCISE & SMART RESPONSE QUALITY:
421
+ - Match the depth of your answer to what the user asked.
422
+ - Do not dump the entire database unless specifically requested.
423
+ - Start directly with the answer — no template greetings like "Hello!" or "I searched...".
424
+ - Keep the interface visually restrained; omit emoji unless one clearly improves recognition of the topic.
425
+
426
+ 9. PROACTIVE MUSIC INVITATION:
427
+ - When discussing a specific song/MV/OST, naturally ask if the user wants to listen.
428
+ - Example: "เพลง FIND THE WAY จังหวะทรงพลังมากเลยค่ะ! อยากให้ Kiku เปิดให้ฟังมั้ยคะ?"
429
+ - If user agrees → call play_music. If user says "Stop" → call stop_music.`,
430
+ }
431
+ };