thincoder 0.7.8 → 0.8.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.
Files changed (96) hide show
  1. package/README.md +32 -13
  2. package/bin/thincoder.mjs +27 -346
  3. package/package.json +1 -1
  4. package/src/agent/dispatch.mjs +98 -0
  5. package/src/agent/helpers.mjs +185 -0
  6. package/src/agent/setup.mjs +117 -0
  7. package/src/agent-tools/goal.mjs +71 -0
  8. package/src/agent-tools/plan.mjs +31 -0
  9. package/src/agent-tools/recent-changes.mjs +23 -0
  10. package/src/agent-tools/skill.mjs +46 -0
  11. package/src/agent-tools/subagent.mjs +113 -0
  12. package/src/agent-tools/task.mjs +67 -0
  13. package/src/agent-tools/verify.mjs +198 -0
  14. package/src/agent-tools.mjs +12 -0
  15. package/src/agent.mjs +90 -1040
  16. package/src/cli/distill-command.mjs +85 -0
  17. package/src/cli/make-agent.mjs +85 -0
  18. package/src/cli/memory-command.mjs +63 -0
  19. package/src/cli/permission.mjs +41 -0
  20. package/src/cli/setup-wizard.mjs +70 -0
  21. package/src/config.mjs +5 -8
  22. package/src/context.mjs +10 -13
  23. package/src/distill.mjs +4 -3
  24. package/src/embedding.mjs +4 -2
  25. package/src/{checkpoint.mjs → git/checkpoint.mjs} +1 -1
  26. package/src/mcp/helpers.mjs +37 -0
  27. package/src/mcp/transport-http.mjs +176 -0
  28. package/src/mcp/transport-stdio.mjs +84 -0
  29. package/src/mcp/transport-ws.mjs +87 -0
  30. package/src/mcp.mjs +4 -428
  31. package/src/memory/code-index.mjs +211 -0
  32. package/src/memory/code-sync.mjs +306 -0
  33. package/src/memory/core.mjs +277 -0
  34. package/src/memory/docs.mjs +262 -0
  35. package/src/memory/schema.mjs +426 -0
  36. package/src/memory.mjs +12 -1403
  37. package/src/provider/core.mjs +239 -0
  38. package/src/provider/index.mjs +6 -0
  39. package/src/provider/rate.mjs +104 -0
  40. package/src/session.mjs +18 -5
  41. package/src/tools/bash.mjs +144 -0
  42. package/src/tools/file.mjs +205 -0
  43. package/src/tools/git.mjs +166 -0
  44. package/src/tools/glob.mjs +51 -0
  45. package/src/tools/grep.mjs +100 -0
  46. package/src/tools/index.mjs +22 -0
  47. package/src/tools/ls.mjs +36 -0
  48. package/src/tools/patch.mjs +226 -0
  49. package/src/tools/repomap-parse.mjs +168 -0
  50. package/src/tools/shared.mjs +257 -0
  51. package/src/tools/system.mjs +336 -0
  52. package/src/tools/web.mjs +121 -0
  53. package/src/tools.mjs +2 -1194
  54. package/src/tui/agent-turn.mjs +254 -0
  55. package/src/tui/ansi.mjs +32 -0
  56. package/src/tui/clipboard.mjs +48 -0
  57. package/src/tui/cmd-auto.mjs +21 -0
  58. package/src/tui/cmd-clear.mjs +26 -0
  59. package/src/tui/cmd-config.mjs +72 -0
  60. package/src/tui/cmd-exit.mjs +5 -0
  61. package/src/tui/cmd-extract.mjs +5 -0
  62. package/src/tui/cmd-goal.mjs +47 -0
  63. package/src/tui/cmd-help.mjs +25 -0
  64. package/src/tui/cmd-init.mjs +91 -0
  65. package/src/tui/cmd-mcp.mjs +146 -0
  66. package/src/tui/cmd-model.mjs +7 -0
  67. package/src/tui/cmd-new.mjs +18 -0
  68. package/src/tui/cmd-plan.mjs +21 -0
  69. package/src/tui/cmd-reindex.mjs +44 -0
  70. package/src/tui/cmd-restore.mjs +39 -0
  71. package/src/tui/cmd-session.mjs +42 -0
  72. package/src/tui/cmd-skills.mjs +17 -0
  73. package/src/tui/cmd-think.mjs +56 -0
  74. package/src/tui/config-helpers.mjs +34 -0
  75. package/src/tui/distill-cmd.mjs +45 -0
  76. package/src/tui/index.mjs +330 -0
  77. package/src/tui/interaction.mjs +79 -0
  78. package/src/tui/key-handler.mjs +267 -0
  79. package/src/tui/layout.mjs +115 -0
  80. package/src/tui/pickers.mjs +279 -0
  81. package/src/tui/render-frame.mjs +304 -0
  82. package/src/tui/render.mjs +205 -0
  83. package/src/tui/slash-commands.mjs +138 -0
  84. package/src/tui/startup.mjs +113 -0
  85. package/src/tui/wizard.mjs +168 -0
  86. package/src/tui-render.mjs +4 -0
  87. package/src/tui.mjs +3 -2566
  88. package/src/provider.mjs +0 -383
  89. /package/src/{gitmem.mjs → git/gitmem.mjs} +0 -0
  90. /package/src/{coder-overlay.md → prompts/coder.md} +0 -0
  91. /package/src/{discipline-rules.md → prompts/discipline.md} +0 -0
  92. /package/src/{explore-overlay.md → prompts/explore.md} +0 -0
  93. /package/src/{main-overlay.md → prompts/main.md} +0 -0
  94. /package/src/{plan-overlay.md → prompts/plan.md} +0 -0
  95. /package/src/{SYSTEM_PROMPT.md → prompts/system.md} +0 -0
  96. /package/src/{repomap.mjs → tools/repomap.mjs} +0 -0
package/src/provider.mjs DELETED
@@ -1,383 +0,0 @@
1
- /**
2
- * provider.mjs — LLM 调用层
3
- * 原生 fetch 直连 OpenAI 兼容协议,SSE 流式,零依赖。
4
- * 覆盖:OpenAI / DeepSeek / Moonshot / Ollama / 一切 OpenAI 兼容端点。
5
- * 模型私有能力(Kimi/Qwen Partial Mode、DeepSeek Prefix Completion)由 config.mjs 的规格表声明,这里只按能力开关分支。
6
- */
7
-
8
- import { specForModel } from "./config.mjs"
9
-
10
- export const RETRYABLE_STATUS = new Set([408, 409, 425, 429, 500, 502, 503, 504])
11
- const MAX_RETRIES = 3
12
- const MAX_CONTINUATIONS = 3 // Partial Mode 截断续写上限(防异常的无限 length 循环)
13
- // 429 的专项退避:RPM/TPM 按 60s 窗口计(账户级独立计数器),通用退避的 1s/2s/4s 等不出窗口
14
- const RATE_LIMIT_BACKOFF_MS = [15_000, 30_000, 60_000]
15
-
16
- /**
17
- * 测试钩子:睡眠/时钟/窗口长度可替换(离线测试不能真等 60s)。
18
- * 生产代码不要直接调 setTimeout/sleep,统一走这里。
19
- */
20
- export const _rateHooks = {
21
- sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
22
- now: () => Date.now(),
23
- windowMs: 60_000,
24
- }
25
-
26
- // ---------------------------------------------------------------- TPM/RPM 主动节流闸门
27
-
28
- /**
29
- * 滑动窗口闸门:发请求前若 窗口内已耗 + 本次估算 超预算,就睡到最早的记录滑出窗口。
30
- * 按 baseURL+apiKey 记账(限速是账户级);预算来自 provider.tpm / provider.rpm,
31
- * 不配则闸门关闭(429 反应式退避仍然生效)。主循环/压缩摘要/子 agent/截断续写全走这里。
32
- */
33
- const rateWindows = new Map() // key → { tokens: [{ts, n}], requests: [ts] }
34
-
35
- function rateKey(provider) {
36
- return `${provider.baseURL}|${provider.apiKey ?? ""}`
37
- }
38
-
39
- /** 粗估文本 token 数(与 context.mjs 同口径:ASCII/4 + 非 ASCII/1;context 依赖本模块,不能反向 import) */
40
- function estimateText(s) {
41
- let nonAscii = 0
42
- for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) > 0x7f) nonAscii++
43
- return Math.ceil((s.length - nonAscii) / 4) + nonAscii
44
- }
45
-
46
- /** 本次请求的 prompt 估算:messages 正文 + 思考链 + tool_calls 参数 + tools schema */
47
- function estimateRequestTokens(body) {
48
- let tokens = 0
49
- for (const m of body.messages ?? []) {
50
- if (typeof m.content === "string") tokens += estimateText(m.content)
51
- if (typeof m.reasoning_content === "string") tokens += estimateText(m.reasoning_content)
52
- for (const tc of m.tool_calls ?? []) {
53
- tokens += estimateText(tc.function?.name ?? "") + estimateText(tc.function?.arguments ?? "")
54
- }
55
- }
56
- if (body.tools) tokens += estimateText(JSON.stringify(body.tools))
57
- return tokens
58
- }
59
-
60
- /** 闸门:超预算则睡到窗口腾出空间;onWait({phase:"gate", seconds}) 供 UI 展示等待 */
61
- async function rateGate(provider, estimated, onWait, signal) {
62
- // 单次请求估算已超过 TPM 预算时闸门无意义(等到天荒地老也塞不下),放行由服务端裁决
63
- const tpm = provider.tpm != null && estimated <= provider.tpm ? provider.tpm : null
64
- const rpm = provider.rpm ?? null
65
- if (tpm == null && rpm == null) return
66
- const w = rateWindows.get(rateKey(provider)) ?? { tokens: [], requests: [] }
67
- rateWindows.set(rateKey(provider), w)
68
- for (;;) {
69
- const now = _rateHooks.now()
70
- const cutoff = now - _rateHooks.windowMs
71
- w.tokens = w.tokens.filter((e) => e.ts > cutoff)
72
- w.requests = w.requests.filter((ts) => ts > cutoff)
73
- const usedTokens = w.tokens.reduce((s, e) => s + e.n, 0)
74
- const overTokens = tpm != null ? usedTokens + estimated - tpm : 0
75
- const overRequests = rpm != null ? w.requests.length + 1 - rpm : 0
76
- if (overTokens <= 0 && overRequests <= 0) break
77
- let waitMs = _rateHooks.windowMs
78
- if (overTokens > 0) {
79
- // tokens 按时间升序:累加最早若干条,过期量足够腾出空间时的过期时刻
80
- let freed = 0
81
- for (const e of w.tokens) {
82
- freed += e.n
83
- if (freed >= overTokens) {
84
- waitMs = Math.min(waitMs, e.ts + _rateHooks.windowMs - now)
85
- break
86
- }
87
- }
88
- }
89
- if (overRequests > 0) {
90
- // 等第 overRequests-1 条(升序)滑出,窗口内请求数即降回 rpm-1
91
- waitMs = Math.min(waitMs, w.requests[overRequests - 1] + _rateHooks.windowMs - now)
92
- }
93
- waitMs = Math.max(waitMs, 50)
94
- onWait?.({ phase: "gate", seconds: Math.ceil(waitMs / 1000) })
95
- await _rateHooks.sleep(waitMs)
96
- if (signal?.aborted) return
97
- }
98
- }
99
-
100
- /** 记账:响应回来后按实测 usage 记(无 usage 用发送前的估算兜底);被拒的请求不记(服务端未处理) */
101
- function recordRate(provider, estimated, usage) {
102
- if (provider.tpm == null && provider.rpm == null) return
103
- const w = rateWindows.get(rateKey(provider)) ?? { tokens: [], requests: [] }
104
- rateWindows.set(rateKey(provider), w)
105
- const now = _rateHooks.now()
106
- w.requests.push(now)
107
- // TPM 按输入+输出总量计(Moonshot 口径)
108
- w.tokens.push({ ts: now, n: usage ? (usage.prompt_tokens ?? estimated) + (usage.completion_tokens ?? 0) : estimated })
109
- }
110
-
111
- /**
112
- * 创建 provider。config: { baseURL, apiKey, model, maxTokens?, temperature?, thinking?, reasoningEffort?, tpm?, rpm? }
113
- * thinking: { type: "enabled"|"disabled" } 思维模式开关
114
- * reasoningEffort: "low"|"high"|"max" 推理强度(DeepSeek/Kimi/GLM 通用)
115
- * tpm / rpm: 主动节流预算(tokens/分钟、请求数/分钟,按账户限速等级自配),不配则闸门关闭
116
- */
117
- export function createProvider(config) {
118
- if (!config?.baseURL) throw new Error("provider config: baseURL is required")
119
- if (!config?.apiKey) throw new Error("provider config: apiKey is required (config file or THINCODER_API_KEY env)")
120
- if (!config?.model) throw new Error("provider config: model is required")
121
- return {
122
- baseURL: config.baseURL.replace(/\/+$/, ""),
123
- apiKey: config.apiKey,
124
- model: config.model,
125
- maxTokens: config.maxTokens,
126
- temperature: config.temperature,
127
- thinking: config.thinking,
128
- reasoningEffort: config.reasoningEffort,
129
- tpm: config.tpm,
130
- rpm: config.rpm,
131
- }
132
- }
133
-
134
- /**
135
- * 流式对话。
136
- * messages: OpenAI 格式数组; tools: OpenAI tools schema(可选)
137
- * onToken(text): 正文流式回调; onReasoning(text): 思考流回调(DeepSeek-R1 类模型)
138
- * onWait({phase, seconds}): 节流等待回调(phase: "gate"=主动节流 / "retry"=429 退避)
139
- * signal: AbortSignal(可选)
140
- * 返回 { content, reasoning, toolCalls: [{id, name, arguments}], usage, finishReason }
141
- * 注意:toolCalls[i].arguments 是 JSON 字符串,调用方负责 parse
142
- *
143
- * 截断续写(按规格表能力门控,未声明的模型原样返回截断结果):
144
- * finish_reason=length 且已有正文时,把已输出内容作为前缀 assistant 消息回传,
145
- * 模型接着续写而非丢弃重跑。思考阶段被截断(content 为空)时无前缀可续,直接返回。
146
- * - partialMode(Kimi / Qwen):assistant 消息带 partial:true;K3 思考续写需回传 reasoning_content
147
- * - prefixMode(DeepSeek):assistant 消息带 prefix:true,且须走 /beta 端点;
148
- * 思考模式不支持前缀续写,已产出 reasoning 时放弃续写
149
- */
150
- export async function chat(provider, { messages, tools, onToken, onReasoning, onWait, signal }) {
151
- const spec = specForModel(provider.model)
152
- const body = {
153
- model: provider.model,
154
- messages,
155
- stream: true,
156
- stream_options: { include_usage: true },
157
- }
158
- if (provider.maxTokens) body.max_tokens = provider.maxTokens
159
- if (provider.temperature != null) {
160
- // 按规格表裁剪 temperature:GLM [0,1] 限两位小数,DeepSeek ≤2,未声明则不裁剪
161
- let t = provider.temperature
162
- if (spec.tempRange) {
163
- t = Math.min(spec.tempRange[1], Math.max(spec.tempRange[0], t))
164
- t = Math.round(t * 100) / 100
165
- }
166
- body.temperature = t
167
- }
168
- if (provider.thinking) body.thinking = provider.thinking
169
- if (provider.reasoningEffort) {
170
- // 按规格表校验 reasoning_effort 枚举:不在枚举内则报错,不映射、不猜测
171
- if (spec.reasoningEffortEnum && !spec.reasoningEffortEnum.includes(provider.reasoningEffort)) {
172
- throw new Error(
173
- `reasoning_effort "${provider.reasoningEffort}" not supported by model "${provider.model}"; ` +
174
- `valid values: ${spec.reasoningEffortEnum.join(", ")}`
175
- )
176
- }
177
- body.reasoning_effort = provider.reasoningEffort
178
- }
179
- if (tools?.length) body.tools = tools
180
-
181
- // TPM/RPM 主动节流:超预算先在本地睡到窗口腾出空间,不打 429 碰运气
182
- const estimated = estimateRequestTokens(body)
183
- await rateGate(provider, estimated, onWait, signal)
184
-
185
- const response = await requestWithRetry(provider, body, signal, onWait)
186
- const result = await readSSE(response, { onToken, onReasoning })
187
- recordRate(provider, estimated, result.usage)
188
-
189
- // 截断续写:仅规格表声明续写协议的模型(其他端点不认识 partial/prefix 字段,可能 400)
190
- if (!spec.partialMode && !spec.prefixMode) return result
191
- // DeepSeek prefix 续写不支持思考模式,已产出 reasoning 时无前缀协议可用
192
- if (spec.prefixMode && !spec.partialMode && result.reasoning) return result
193
- for (let n = 0; result.finishReason === "length" && result.content && n < MAX_CONTINUATIONS; n++) {
194
- const continued = await chat(spec.prefixMode ? { ...provider, baseURL: betaBaseURL(provider.baseURL) } : provider, {
195
- messages: [
196
- ...messages,
197
- spec.partialMode
198
- ? {
199
- role: "assistant",
200
- content: result.content,
201
- partial: true,
202
- // K3 思考模式续写必须回传 reasoning_content
203
- ...(result.reasoning ? { reasoning_content: result.reasoning } : {}),
204
- }
205
- : { role: "assistant", content: result.content, prefix: true },
206
- ],
207
- tools,
208
- onToken,
209
- onReasoning,
210
- onWait,
211
- signal,
212
- })
213
- result.content += continued.content
214
- result.reasoning += continued.reasoning ?? ""
215
- for (const tc of continued.toolCalls ?? []) {
216
- if (tc.index == null) { result.toolCalls = continued.toolCalls; break }
217
- const s = result.toolCalls[tc.index] ??= { id: "", name: "", arguments: "" }
218
- if (tc.id) s.id = tc.id
219
- s.name += tc.name ?? ""
220
- s.arguments += tc.arguments ?? ""
221
- }
222
- result.finishReason = continued.finishReason
223
- if (continued.usage) {
224
- const sum = (k) => (result.usage?.[k] ?? 0) + (continued.usage[k] ?? 0)
225
- result.usage = {
226
- prompt_tokens: sum("prompt_tokens"),
227
- completion_tokens: sum("completion_tokens"),
228
- total_tokens: sum("total_tokens"),
229
- // 缓存命中/未命中也要累计(DeepSeek 计费与状态栏展示依赖这两个字段)
230
- prompt_cache_hit_tokens: sum("prompt_cache_hit_tokens"),
231
- prompt_cache_miss_tokens: sum("prompt_cache_miss_tokens"),
232
- }
233
- }
234
- }
235
- return result
236
- }
237
-
238
- /**
239
- * 拉取端点可用模型列表(GET /v1/models)。
240
- * 返回模型 id 数组;端点不支持时抛错。
241
- */
242
- export async function listModels(provider, { signal } = {}) {
243
- const response = await fetch(`${provider.baseURL}/models`, {
244
- headers: { Authorization: `Bearer ${provider.apiKey}` },
245
- signal,
246
- })
247
- if (!response.ok) {
248
- const text = await response.text().catch(() => "")
249
- throw new Error(`GET /models failed ${response.status}: ${text}`)
250
- }
251
- const data = await response.json()
252
- return (data.data ?? []).map((m) => m.id).filter(Boolean).sort()
253
- }
254
-
255
- /**
256
- * 带重试的请求:网络错误与 5xx 指数退避(1s/2s/4s);429 专项处理——
257
- * 有 Retry-After 以它为准,没有按 15s/30s/60s 退避(RPM/TPM 是 60s 窗口,秒级退避等不出去)。
258
- * 配额/余额错误(如 exceeded_current_quota_error)与限速同状态码但语义不同:重试无用,直接抛。
259
- */
260
- async function requestWithRetry(provider, body, signal, onWait) {
261
- let lastError
262
- let lastWas429 = false
263
- let rateLimitHits = 0 // 连续 429 计数(退避档位用,与 attempt 解耦)
264
- for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
265
- if (attempt > 0 && !lastWas429) await _rateHooks.sleep(2 ** (attempt - 1) * 1000)
266
- lastWas429 = false
267
-
268
- let response
269
- try {
270
- response = await fetch(`${provider.baseURL}${provider.chatPath ?? "/chat/completions"}`, {
271
- method: "POST",
272
- headers: {
273
- "Content-Type": "application/json",
274
- Authorization: `Bearer ${provider.apiKey}`,
275
- },
276
- body: JSON.stringify(body),
277
- signal,
278
- })
279
- } catch (error) {
280
- if (error.name === "AbortError") throw error
281
- lastError = error // 网络层错误,可重试
282
- continue
283
- }
284
-
285
- if (response.ok) return response
286
-
287
- const text = await response.text().catch(() => "")
288
- const message = `LLM API error ${response.status}: ${text}`
289
- if (isQuotaError(text)) throw new Error(message)
290
- if (response.status === 429) {
291
- const retryAfter = Number(response.headers.get("retry-after"))
292
- const waitMs =
293
- Number.isFinite(retryAfter) && retryAfter > 0
294
- ? retryAfter * 1000
295
- : RATE_LIMIT_BACKOFF_MS[Math.min(rateLimitHits++, RATE_LIMIT_BACKOFF_MS.length - 1)]
296
- lastError = new Error(message)
297
- lastWas429 = true
298
- if (attempt < MAX_RETRIES) {
299
- onWait?.({ phase: "retry", seconds: Math.ceil(waitMs / 1000) })
300
- await _rateHooks.sleep(waitMs)
301
- }
302
- continue
303
- }
304
- if (RETRYABLE_STATUS.has(response.status)) {
305
- lastError = new Error(message)
306
- continue
307
- }
308
- throw new Error(message)
309
- }
310
- throw lastError
311
- }
312
-
313
- /** 配额/余额错误(重试无意义):错误体 type 含 quota,如 Moonshot exceeded_current_quota_error */
314
- function isQuotaError(text) {
315
- try {
316
- const type = JSON.parse(text)?.error?.type
317
- return typeof type === "string" && type.includes("quota")
318
- } catch {
319
- return false
320
- }
321
- }
322
-
323
- /** 解析 SSE 流,累积正文/思考/tool_calls */
324
- async function readSSE(response, { onToken, onReasoning }) {
325
- const result = { content: "", reasoning: "", toolCalls: [], usage: null, finishReason: null }
326
- const decoder = new TextDecoder()
327
- let buffer = ""
328
-
329
- const processLines = (lines) => {
330
- for (const line of lines) {
331
- if (!line.startsWith("data:")) continue
332
- const data = line.slice(5).trim()
333
- if (!data || data === "[DONE]") continue
334
-
335
- let json
336
- try {
337
- json = JSON.parse(data)
338
- } catch {
339
- continue // 忽略坏行,流不能因为一帧坏数据断掉
340
- }
341
-
342
- if (json.usage) result.usage = json.usage
343
- const choice = json.choices?.[0]
344
- if (!choice) continue
345
- if (choice.finish_reason) result.finishReason = choice.finish_reason
346
-
347
- const delta = choice.delta ?? {}
348
- if (delta.reasoning_content) {
349
- result.reasoning += delta.reasoning_content
350
- onReasoning?.(delta.reasoning_content)
351
- }
352
- if (delta.content) {
353
- result.content += delta.content
354
- onToken?.(delta.content)
355
- }
356
- // tool_calls 按 index 分槽累积,arguments 是分片到达的需拼接;
357
- // name 个别 API(GLM 偶尔)会重发完整 name 而非增量,用 += 会拼成 "readread"——只取第一次非空值
358
- for (const tc of delta.tool_calls ?? []) {
359
- const slot = (result.toolCalls[tc.index] ??= { id: "", name: "", arguments: "" })
360
- if (tc.id) slot.id = tc.id
361
- if (tc.function?.name && !slot.name) slot.name = tc.function.name
362
- if (tc.function?.arguments) slot.arguments += tc.function.arguments
363
- }
364
- }
365
- }
366
-
367
- if (!response.body) throw new Error("No stream response body")
368
- for await (const chunk of response.body) {
369
- buffer += decoder.decode(chunk, { stream: true })
370
- const lines = buffer.split("\n")
371
- buffer = lines.pop() // 最后半行留到下一轮
372
- processLines(lines)
373
- }
374
- // flush 解码器内部残留(流以不完整 UTF-8 序列截断时不丢尾部字节),并处理没有换行结尾的尾行
375
- buffer += decoder.decode()
376
- processLines(buffer.split("\n"))
377
- return result
378
- }
379
-
380
- /** DeepSeek Prefix Completion 只在 /beta 端点开放:.../v1 → .../beta */
381
- function betaBaseURL(baseURL) {
382
- return baseURL.replace(/\/v1$/, "/beta")
383
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes