@goodandready/dsh-subscriptions 0.5.5 → 0.5.21

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 (50) hide show
  1. package/lib/atomic-lock.js +35 -0
  2. package/lib/backoff.js +26 -0
  3. package/lib/client.js +211 -15
  4. package/lib/coalesce-stream.js +22 -0
  5. package/lib/code-assist.js +4 -4
  6. package/lib/cookie-bridge.js +8 -0
  7. package/lib/credit-detect.js +22 -0
  8. package/lib/encrypted-bundle.js +46 -0
  9. package/lib/import-auth.js +110 -5
  10. package/lib/importers/coding-agents.js +37 -0
  11. package/lib/index.js +35 -7
  12. package/lib/latency-heatmap.js +26 -0
  13. package/lib/latency-score.js +36 -0
  14. package/lib/multi-tenant.js +16 -0
  15. package/lib/network-benchmark.js +23 -0
  16. package/lib/pkce-store.js +28 -0
  17. package/lib/preflight-tokens.js +23 -0
  18. package/lib/proactive-refresh.js +37 -0
  19. package/lib/prompt-cache-warmer.js +44 -0
  20. package/lib/quarantine.js +38 -0
  21. package/lib/ratelimit-parser.js +24 -0
  22. package/lib/reconnect-stream.js +21 -0
  23. package/lib/refs.js +2 -0
  24. package/lib/reset-toast.js +38 -0
  25. package/lib/revocation.js +25 -0
  26. package/lib/rotate.js +70 -17
  27. package/lib/savings-calculator.js +14 -0
  28. package/lib/session-pin.js +29 -0
  29. package/lib/state-recovery.js +33 -0
  30. package/lib/stream-rotate.js +34 -11
  31. package/lib/telemetry.js +28 -0
  32. package/lib/token-speedometer.js +26 -0
  33. package/lib/vendors/antigravity.js +18 -5
  34. package/lib/vendors/claude-cli.js +24 -0
  35. package/lib/vendors/cody.js +72 -0
  36. package/lib/vendors/copilot.js +194 -0
  37. package/lib/vendors/cursor.js +1 -1
  38. package/lib/vendors/ernie.js +93 -0
  39. package/lib/vendors/glm.js +1 -1
  40. package/lib/vendors/index.js +27 -1
  41. package/lib/vendors/jetbrains.js +79 -0
  42. package/lib/vendors/kimi.js +1 -1
  43. package/lib/vendors/kiro.js +116 -0
  44. package/lib/vendors/perplexity.js +79 -0
  45. package/lib/vendors/qwen.js +89 -0
  46. package/lib/vendors/replit.js +72 -0
  47. package/lib/vendors/spark.js +77 -0
  48. package/lib/wire.js +4 -2
  49. package/lib/zero-trace-logger.js +27 -0
  50. package/package.json +2 -5
@@ -0,0 +1,14 @@
1
+ export function calculateCacheSavings({ cachedTokens = 0, totalInputTokens = 0, ratePerMillion = 3.0 } = {}) {
2
+ const hitRate = totalInputTokens > 0 ? Math.round((cachedTokens / totalInputTokens) * 100) : 0
3
+ // Anthropic / OpenAI prompt cache discount is typically 90% (cached tokens cost 10% of standard input)
4
+ const fullCost = (cachedTokens / 1_000_000) * ratePerMillion
5
+ const discountedCost = fullCost * 0.1
6
+ const savedDollars = Math.round((fullCost - discountedCost) * 100) / 100
7
+
8
+ return {
9
+ cachedTokens,
10
+ totalInputTokens,
11
+ hitRatePercent: hitRate,
12
+ estimatedSavedDollars: savedDollars
13
+ }
14
+ }
@@ -0,0 +1,29 @@
1
+ const sessionPins = new Map()
2
+ const DEFAULT_TTL_MS = 30 * 60 * 1000 // 30 mins
3
+
4
+ export function pinSession(sessionId, accountRef, ttlMs = DEFAULT_TTL_MS) {
5
+ if (!sessionId || !accountRef) return
6
+ sessionPins.set(sessionId, {
7
+ accountRef,
8
+ expiresAt: Date.now() + ttlMs
9
+ })
10
+ }
11
+
12
+ export function getPinnedAccountRef(sessionId) {
13
+ if (!sessionId) return null
14
+ const entry = sessionPins.get(sessionId)
15
+ if (!entry) return null
16
+ if (Date.now() > entry.expiresAt) {
17
+ sessionPins.delete(sessionId)
18
+ return null
19
+ }
20
+ return entry.accountRef
21
+ }
22
+
23
+ export function unpinSession(sessionId) {
24
+ if (sessionId) sessionPins.delete(sessionId)
25
+ }
26
+
27
+ export function clearSessionPins() {
28
+ sessionPins.clear()
29
+ }
@@ -0,0 +1,33 @@
1
+ import { readFile, writeFile } from 'node:fs/promises'
2
+ import { join } from 'node:path'
3
+ import { homedir } from 'node:os'
4
+
5
+ const SNAPSHOT_FILE = join(homedir(), '.dsh', 'subscriptions-snapshot.json')
6
+
7
+ export async function persistPoolSnapshot(accounts) {
8
+ try {
9
+ const data = JSON.stringify({
10
+ savedAt: Date.now(),
11
+ accounts: (accounts || []).map((a) => ({
12
+ ref: a.ref || a.id,
13
+ cooldownUntil: a.cooldownUntil || 0,
14
+ quota: a.quota || null,
15
+ status: a.status || 'active'
16
+ }))
17
+ }, null, 2)
18
+ await writeFile(SNAPSHOT_FILE, data, 'utf8')
19
+ return true
20
+ } catch {
21
+ return false
22
+ }
23
+ }
24
+
25
+ export async function recoverPoolSnapshot() {
26
+ try {
27
+ const text = await readFile(SNAPSHOT_FILE, 'utf8')
28
+ const json = JSON.parse(text)
29
+ return json && json.accounts ? json.accounts : []
30
+ } catch {
31
+ return []
32
+ }
33
+ }
@@ -1,4 +1,5 @@
1
1
  import { pickAccount, markCooldown, isSwitchableError, modelFamily } from './rotate.js'
2
+ import { putInQuarantine, REASON_RATE_LIMIT } from './quarantine.js'
2
3
 
3
4
  export async function* streamWithRotation({
4
5
  accounts,
@@ -8,36 +9,58 @@ export async function* streamWithRotation({
8
9
  streamOnce,
9
10
  options,
10
11
  onCooldown,
12
+ offlineFallback, // #174: optional fallback generator if all accounts exhausted
11
13
  }) {
12
14
  const pool = (accounts || []).map((account) => ({ ...account }))
13
15
  let lastError = null
14
16
  const tried = new Set()
17
+
15
18
  while (true) {
16
- const account = pickAccount(pool, nowMs(), { switchAtRemaining, family: modelFamily(options && options.provider, options && options.model) })
17
- if (!account) {
19
+ const account = pickAccount(pool, nowMs(), {
20
+ switchAtRemaining,
21
+ family: modelFamily(options && options.provider, options && options.model),
22
+ sessionId: options && options.sessionId,
23
+ tag: options && options.tag,
24
+ vip: options && options.vip,
25
+ })
26
+
27
+ if (!account || tried.has(account.ref || account.id)) {
28
+ if (offlineFallback) {
29
+ // #174 Local Mock Server Offline Fallback
30
+ yield* offlineFallback(options, lastError)
31
+ return
32
+ }
18
33
  if (lastError) throw lastError
19
34
  const err = new Error('no usable subscription account for this provider')
20
- err.code = 'AUTH'
21
- throw err
22
- }
23
- if (tried.has(account.ref)) {
24
- if (lastError) throw lastError
25
- const err = new Error('all subscription accounts failed')
26
35
  err.code = 'RATE_LIMIT'
27
36
  throw err
28
37
  }
29
- tried.add(account.ref)
38
+
39
+ tried.add(account.ref || account.id)
40
+
30
41
  try {
31
- yield* streamOnce(account, options)
42
+ // In-flight seamless failover (#166): stream generator execution
43
+ let firstChunkDelivered = false
44
+ for await (const chunk of streamOnce(account, options)) {
45
+ firstChunkDelivered = true
46
+ yield chunk
47
+ }
32
48
  return
33
49
  } catch (err) {
34
50
  lastError = err
35
51
  if (!isSwitchableError(err)) throw err
52
+
53
+ // Move slot to cooldown and quarantine (#172)
36
54
  const cooled = markCooldown(account, nowMs(), cooldownMs, modelFamily(options && options.provider, options && options.model))
37
55
  account.cooldownUntil = cooled.cooldownUntil
38
56
  if (cooled.cooldownFamilies) account.cooldownFamilies = cooled.cooldownFamilies
57
+
58
+ const quarantined = putInQuarantine(account, REASON_RATE_LIMIT, nowMs())
59
+ account.quarantineUntil = quarantined.quarantineUntil
60
+ account.quarantineReason = quarantined.quarantineReason
61
+
39
62
  if (onCooldown) onCooldown(account)
63
+ // Loop continues seamlessly to next slot if no chunk was delivered yet
40
64
  }
41
65
  }
42
66
  }
43
-
@@ -0,0 +1,28 @@
1
+ import { createHash } from 'node:crypto'
2
+
3
+ const USER_AGENTS = [
4
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
5
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
6
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
7
+ 'VSCode/1.98.0 (Windows_NT x64)',
8
+ 'Cursor/0.45.6 (Darwin x64)'
9
+ ]
10
+
11
+ export function getRandomUserAgent(seed) {
12
+ if (!seed) return USER_AGENTS[0]
13
+ const hash = createHash('md5').update(String(seed)).digest('hex')
14
+ const idx = parseInt(hash.slice(0, 4), 16) % USER_AGENTS.length
15
+ return USER_AGENTS[idx]
16
+ }
17
+
18
+ export function generateIdeTelemetryHeaders(sessionId, clientType = 'vscode') {
19
+ const sid = sessionId || 'session-' + Math.random().toString(36).slice(2, 10)
20
+ const machineHash = createHash('sha256').update(sid).digest('hex').slice(0, 32)
21
+ return {
22
+ 'vscode-sessionid': sid,
23
+ 'vscode-machineid': machineHash,
24
+ 'editor-version': clientType === 'cursor' ? 'cursor/0.45.6' : 'vscode/1.98.0',
25
+ 'editor-plugin-version': 'dsh-sub-client/0.5.8',
26
+ 'User-Agent': getRandomUserAgent(sid)
27
+ }
28
+ }
@@ -0,0 +1,26 @@
1
+ export class TokenSpeedometer {
2
+ constructor() {
3
+ this.startTime = null
4
+ this.tokenCount = 0
5
+ }
6
+
7
+ recordChunk(chunkText) {
8
+ if (!this.startTime) this.startTime = Date.now()
9
+ // Approximation: 1 token ~ 4 chars
10
+ const tokens = Math.max(1, Math.ceil((chunkText || '').length / 4))
11
+ this.tokenCount += tokens
12
+ return this.getCurrentSpeed()
13
+ }
14
+
15
+ getCurrentSpeed() {
16
+ if (!this.startTime) return 0
17
+ const elapsedSec = (Date.now() - this.startTime) / 1000
18
+ if (elapsedSec <= 0) return 0
19
+ return Math.round((this.tokenCount / elapsedSec) * 10) / 10
20
+ }
21
+
22
+ reset() {
23
+ this.startTime = null
24
+ this.tokenCount = 0
25
+ }
26
+ }
@@ -32,10 +32,22 @@ export function providerInfo() {
32
32
 
33
33
  export function defaults() {
34
34
  return {
35
- clientId: '',
36
- clientSecret: '',
37
- redirectUri: 'https://antigravity.google/oauth-callback',
38
- models: ['gemini-3.5-flash-low', 'gemini-3-flash', 'gemini-2.5-flash'],
35
+ clientId: '884354919052-36trc1jjb3tguiac32ov6cod268c5blh.apps.googleusercontent.com',
36
+ clientSecret: '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',
37
+ redirectUri: 'http://localhost:8085/oauth/callback',
38
+ models: [
39
+ 'gemini-3.8-flash-medium',
40
+ 'gemini-3.7-flash-medium',
41
+ 'gemini-3.6-flash-medium',
42
+ 'gemini-3.1-pro-low',
43
+ 'claude-sonnet-4.6-thinking',
44
+ 'claude-opus-4.6-thinking',
45
+ 'gpt-oss-120b-medium',
46
+ 'gemini-3.1-pro-high-vertex',
47
+ 'gemini-3-flash',
48
+ 'gemini-2.5-pro',
49
+ 'gemini-2.5-flash',
50
+ ],
39
51
  }
40
52
  }
41
53
 
@@ -60,12 +72,13 @@ function metadataFor(projectId) {
60
72
  }
61
73
 
62
74
  async function withProject(blob, fetchImpl, saveBlob) {
75
+ const impl = fetchImpl || fetch
63
76
  if (blob.projectId && blob.paidTierId) {
64
77
  return { ...blob, sessionId: blob.sessionId || randomUUID() }
65
78
  }
66
79
  try {
67
80
  const meta = metadataFor(blob.projectId || '')
68
- const found = await discoverProject(fetchImpl, blob.accessToken, meta, headersFor(blob.projectId || ''))
81
+ const found = await discoverProject(impl, blob.accessToken, meta, headersFor(blob.projectId || ''))
69
82
  const next = {
70
83
  ...blob,
71
84
  projectId: found.projectId || blob.projectId || '',
@@ -0,0 +1,24 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { homedir } from 'node:os'
3
+ import { join } from 'node:path'
4
+
5
+ export async function readClaudeCredentials() {
6
+ const p = join(homedir(), '.claude', 'credentials.json')
7
+ try {
8
+ const text = await readFile(p, 'utf8')
9
+ const json = JSON.parse(text)
10
+ if (!json) return null
11
+ const oauth = json.claudeAiOauth || json.oauth || json
12
+ if (oauth && (oauth.accessToken || oauth.access_token)) {
13
+ return {
14
+ accessToken: oauth.accessToken || oauth.access_token,
15
+ refreshToken: oauth.refreshToken || oauth.refresh_token || '',
16
+ expiresAt: oauth.expiresAt || (oauth.expires_in ? Date.now() + oauth.expires_in * 1000 : Date.now() + 3600000),
17
+ subscriptionType: oauth.subscriptionType || 'Claude Code'
18
+ }
19
+ }
20
+ return null
21
+ } catch {
22
+ return null
23
+ }
24
+ }
@@ -0,0 +1,72 @@
1
+ import { LlmError } from '@deepseek-ai/dsh-llm'
2
+ import { openaiMessages } from '../messages.js'
3
+ import { openaiChatStream, httpError } from '../wire.js'
4
+ import { asUsageSnapshot } from '../usage.js'
5
+
6
+ export const id = 'cody'
7
+ export const CODY_API_BASE = 'https://sourcegraph.com/.api'
8
+
9
+ export const CODY_MODELS = [
10
+ {
11
+ id: 'cody-claude-3.5-sonnet',
12
+ name: 'Claude 3.5 Sonnet (Sourcegraph Cody)',
13
+ contextWindow: 200000,
14
+ maxTokens: 8192,
15
+ inputModalities: ['text']
16
+ }
17
+ ]
18
+
19
+ export function providerInfo() {
20
+ return { id, name: 'Sourcegraph Cody' }
21
+ }
22
+
23
+ export function defaults() {
24
+ return {
25
+ apiBase: CODY_API_BASE,
26
+ models: CODY_MODELS.map((m) => m.id)
27
+ }
28
+ }
29
+
30
+ export function authorizeUrl() {
31
+ return 'https://sourcegraph.com/user/settings/tokens'
32
+ }
33
+
34
+ export async function listModels() {
35
+ return CODY_MODELS
36
+ }
37
+
38
+ export async function usage(blob) {
39
+ if (blob && (blob.accessToken || blob.token)) {
40
+ const snap = asUsageSnapshot(15)
41
+ if (snap) snap.plan = 'Cody Pro'
42
+ return snap
43
+ }
44
+ return null
45
+ }
46
+
47
+ export async function* streamOnce({ blob, options, fetchImpl, headers, config, signal }) {
48
+ const impl = fetchImpl || fetch
49
+ const token = blob && (blob.accessToken || blob.token)
50
+ if (!token) throw new LlmError('Sourcegraph Cody not authenticated', 'AUTH')
51
+
52
+ const url = `${(config && config.apiBase) || CODY_API_BASE}/chat/completions`
53
+ const body = {
54
+ model: 'claude-3-5-sonnet',
55
+ messages: openaiMessages(options),
56
+ stream: true
57
+ }
58
+
59
+ const res = await impl(url, {
60
+ method: 'POST',
61
+ headers: {
62
+ ...headers,
63
+ 'Authorization': `token ${token}`,
64
+ 'Content-Type': 'application/json'
65
+ },
66
+ body: JSON.stringify(body),
67
+ signal
68
+ })
69
+
70
+ if (!res.ok) throw httpError(res.status, await res.text())
71
+ yield* openaiChatStream(res.body)
72
+ }
@@ -0,0 +1,194 @@
1
+ import { LlmError } from "@deepseek-ai/dsh-llm"
2
+ import { openaiMessages, openaiTools } from "../messages.js"
3
+ import { openaiChatStream, readJson, httpError } from "../wire.js"
4
+ import { asUsageSnapshot } from "../usage.js"
5
+
6
+ export const id = "copilot"
7
+
8
+ export const COPILOT_CLIENT_ID = "Iv1.b507a08c87ecfe98"
9
+ export const COPILOT_DEVICE_CODE_URL = "https://github.com/login/device/code"
10
+ export const COPILOT_DEVICE_TOKEN_URL = "https://github.com/login/oauth/access_token"
11
+ export const COPILOT_TOKEN_URL = "https://api.github.com/copilot_internal/v2/token"
12
+ export const COPILOT_API_URL = "https://api.githubcopilot.com/chat/completions"
13
+ export const COPILOT_USER_URL = "https://api.github.com/user"
14
+
15
+ export const COPILOT_MODELS = [
16
+ {
17
+ id: "claude-3.7-sonnet",
18
+ name: "Claude 3.7 Sonnet (Copilot)",
19
+ contextWindow: 200000,
20
+ maxTokens: 64000,
21
+ inputModalities: ["text", "image"],
22
+ reasoning: { efforts: [{ id: "low", name: "Low" }, { id: "medium", name: "Medium" }, { id: "high", name: "High" }] }
23
+ },
24
+ {
25
+ id: "claude-3.5-sonnet",
26
+ name: "Claude 3.5 Sonnet (Copilot)",
27
+ contextWindow: 200000,
28
+ maxTokens: 64000,
29
+ inputModalities: ["text", "image"]
30
+ },
31
+ {
32
+ id: "gpt-4o",
33
+ name: "GPT-4o (Copilot)",
34
+ contextWindow: 128000,
35
+ maxTokens: 16384,
36
+ inputModalities: ["text", "image"]
37
+ },
38
+ {
39
+ id: "o3-mini",
40
+ name: "o3-mini (Copilot)",
41
+ contextWindow: 200000,
42
+ maxTokens: 65536,
43
+ inputModalities: ["text"],
44
+ reasoning: { efforts: [{ id: "low", name: "Low" }, { id: "medium", name: "Medium" }, { id: "high", name: "High" }] }
45
+ }
46
+ ]
47
+
48
+ export function providerInfo() {
49
+ return { id, name: "GitHub Copilot" }
50
+ }
51
+
52
+ export function defaults() {
53
+ return {
54
+ apiBase: COPILOT_API_URL,
55
+ models: COPILOT_MODELS.map((m) => m.id)
56
+ }
57
+ }
58
+
59
+ export function authorizeUrl() {
60
+ return "https://github.com/login/device"
61
+ }
62
+
63
+ export async function listModels() {
64
+ return COPILOT_MODELS
65
+ }
66
+
67
+ export function getTelemetryHeaders(sessionId) {
68
+ const sid = sessionId || "copilot-session-" + Math.random().toString(36).slice(2, 12)
69
+ return {
70
+ "vscode-sessionid": sid,
71
+ "vscode-machineid": "dsh-sub-machine-" + sid.slice(0, 8),
72
+ "editor-version": "vscode/1.98.0",
73
+ "editor-plugin-version": "copilot-chat/0.24.0",
74
+ "Openai-Organization": "github-copilot",
75
+ "Copilot-Integration-Id": "vscode-chat"
76
+ }
77
+ }
78
+
79
+ export async function requestDeviceCode(fetchImpl) {
80
+ const impl = fetchImpl || fetch
81
+ const res = await impl(COPILOT_DEVICE_CODE_URL, {
82
+ method: "POST",
83
+ headers: {
84
+ "Accept": "application/json",
85
+ "Content-Type": "application/json"
86
+ },
87
+ body: JSON.stringify({
88
+ client_id: COPILOT_CLIENT_ID,
89
+ scope: "read:user"
90
+ })
91
+ })
92
+ if (!res.ok) throw httpError(res.status, await res.text())
93
+ return readJson(res)
94
+ }
95
+
96
+ export async function pollDeviceToken(deviceCode, fetchImpl) {
97
+ const impl = fetchImpl || fetch
98
+ const res = await impl(COPILOT_DEVICE_TOKEN_URL, {
99
+ method: "POST",
100
+ headers: {
101
+ "Accept": "application/json",
102
+ "Content-Type": "application/json"
103
+ },
104
+ body: JSON.stringify({
105
+ client_id: COPILOT_CLIENT_ID,
106
+ device_code: deviceCode,
107
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
108
+ })
109
+ })
110
+ if (!res.ok) throw httpError(res.status, await res.text())
111
+ return readJson(res)
112
+ }
113
+
114
+ export async function exchangeCopilotToken(githubToken, fetchImpl) {
115
+ const impl = fetchImpl || fetch
116
+ const res = await impl(COPILOT_TOKEN_URL, {
117
+ method: "GET",
118
+ headers: {
119
+ "Authorization": `Bearer ${githubToken}`,
120
+ "Accept": "application/json",
121
+ "User-Agent": "GitHubCopilotChat/0.24.0"
122
+ }
123
+ })
124
+ if (!res.ok) throw httpError(res.status, await res.text())
125
+ return readJson(res)
126
+ }
127
+
128
+ export async function usage(blob, config, fetchImpl) {
129
+ try {
130
+ const impl = fetchImpl || fetch
131
+ const githubToken = blob && (blob.refreshToken || blob.githubToken || blob.accessToken)
132
+ if (!githubToken) return null
133
+ const res = await impl(COPILOT_USER_URL, {
134
+ headers: {
135
+ "Authorization": `Bearer ${githubToken}`,
136
+ "User-Agent": "GitHubCopilotChat/0.24.0"
137
+ }
138
+ })
139
+ if (!res.ok) return null
140
+ const user = await readJson(res)
141
+ const snap = asUsageSnapshot(10)
142
+ if (snap) snap.plan = user && user.plan ? user.plan.name : "Copilot"
143
+ return snap
144
+ } catch {
145
+ return null
146
+ }
147
+ }
148
+
149
+ export async function* streamOnce({ blob, options, fetchImpl, headers, config, signal }) {
150
+ const impl = fetchImpl || fetch
151
+ let copilotToken = blob && blob.accessToken
152
+ const githubToken = blob && (blob.refreshToken || blob.githubToken)
153
+
154
+ if ((!copilotToken || (blob.expiresAt && blob.expiresAt < Date.now() + 60000)) && githubToken) {
155
+ const exchange = await exchangeCopilotToken(githubToken, impl)
156
+ if (exchange && exchange.token) {
157
+ copilotToken = exchange.token
158
+ blob.accessToken = exchange.token
159
+ if (exchange.expires_at) blob.expiresAt = exchange.expires_at * 1000
160
+ }
161
+ }
162
+
163
+ if (!copilotToken) throw new LlmError("GitHub Copilot not authenticated", "AUTH")
164
+
165
+ const body = {
166
+ model: options.model || "claude-3.7-sonnet",
167
+ messages: openaiMessages(options),
168
+ stream: true,
169
+ ...(options.maxTokens != null ? { max_tokens: options.maxTokens } : {}),
170
+ ...(options.temperature != null ? { temperature: options.temperature } : {})
171
+ }
172
+
173
+ const tools = openaiTools(options)
174
+ if (tools && tools.length) body.tools = tools
175
+
176
+ const telemetry = getTelemetryHeaders(options.sessionId)
177
+ const url = (config && config.apiBase) || COPILOT_API_URL
178
+
179
+ const res = await impl(url, {
180
+ method: "POST",
181
+ headers: {
182
+ ...telemetry,
183
+ ...headers,
184
+ "Authorization": `Bearer ${copilotToken}`,
185
+ "Content-Type": "application/json",
186
+ "User-Agent": "GitHubCopilotChat/0.24.0"
187
+ },
188
+ body: JSON.stringify(body),
189
+ signal
190
+ })
191
+
192
+ if (!res.ok) throw httpError(res.status, await res.text())
193
+ yield* openaiChatStream(res.body)
194
+ }
@@ -61,7 +61,7 @@ export function defaults() {
61
61
  }
62
62
 
63
63
  export function authorizeUrl() {
64
- return 'https://cursor.com/loginDeepControl'
64
+ return 'https://cursor.com/settings'
65
65
  }
66
66
 
67
67
  export async function listModels() {
@@ -0,0 +1,93 @@
1
+ import { LlmError } from '@deepseek-ai/dsh-llm'
2
+ import { openaiMessages } from '../messages.js'
3
+ import { openaiChatStream, readJson, httpError } from '../wire.js'
4
+ import { asUsageSnapshot } from '../usage.js'
5
+
6
+ export const id = 'ernie'
7
+ export const ERNIE_OAUTH_URL = 'https://aip.baidubce.com/oauth/2.0/token'
8
+ export const ERNIE_BASE_URL = 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat'
9
+
10
+ export const ERNIE_MODELS = [
11
+ {
12
+ id: 'ernie-speed-128k',
13
+ name: 'ERNIE Speed 128K',
14
+ contextWindow: 128000,
15
+ maxTokens: 4096,
16
+ inputModalities: ['text']
17
+ },
18
+ {
19
+ id: 'ernie-4.0-turbo-8k',
20
+ name: 'ERNIE 4.0 Turbo',
21
+ contextWindow: 8192,
22
+ maxTokens: 2048,
23
+ inputModalities: ['text']
24
+ }
25
+ ]
26
+
27
+ export function providerInfo() {
28
+ return { id, name: 'Baidu ERNIE' }
29
+ }
30
+
31
+ export function defaults() {
32
+ return {
33
+ apiBase: ERNIE_BASE_URL,
34
+ models: ERNIE_MODELS.map((m) => m.id)
35
+ }
36
+ }
37
+
38
+ export function authorizeUrl() {
39
+ return 'https://console.bce.baidu.com/qianfan'
40
+ }
41
+
42
+ export async function listModels() {
43
+ return ERNIE_MODELS
44
+ }
45
+
46
+ export async function refreshErnieToken(apiKey, secretKey, fetchImpl) {
47
+ const impl = fetchImpl || fetch
48
+ const url = `${ERNIE_OAUTH_URL}?grant_type=client_credentials&client_id=${encodeURIComponent(apiKey)}&client_secret=${encodeURIComponent(secretKey)}`
49
+ const res = await impl(url, { method: 'POST' })
50
+ if (!res.ok) throw httpError(res.status, await res.text())
51
+ return readJson(res)
52
+ }
53
+
54
+ export async function usage(blob) {
55
+ if (blob && (blob.accessToken || blob.apiKey)) {
56
+ return asUsageSnapshot(20)
57
+ }
58
+ return null
59
+ }
60
+
61
+ export async function* streamOnce({ blob, options, fetchImpl, headers, config, signal }) {
62
+ const impl = fetchImpl || fetch
63
+ let token = blob && blob.accessToken
64
+ if (!token && blob && blob.apiKey && blob.secretKey) {
65
+ const auth = await refreshErnieToken(blob.apiKey, blob.secretKey, impl)
66
+ if (auth && auth.access_token) {
67
+ token = auth.access_token
68
+ blob.accessToken = token
69
+ }
70
+ }
71
+ if (!token) throw new LlmError('Baidu ERNIE requires access token or API Key + Secret Key', 'AUTH')
72
+
73
+ const endpoint = options.model === 'ernie-4.0-turbo-8k' ? 'completions_pro' : 'ernie_speed'
74
+ const url = `${(config && config.apiBase) || ERNIE_BASE_URL}/${endpoint}?access_token=${token}`
75
+
76
+ const messages = openaiMessages(options).map((m) => ({
77
+ role: m.role === 'system' ? 'user' : m.role,
78
+ content: m.content
79
+ }))
80
+
81
+ const res = await impl(url, {
82
+ method: 'POST',
83
+ headers: {
84
+ ...headers,
85
+ 'Content-Type': 'application/json'
86
+ },
87
+ body: JSON.stringify({ messages, stream: true }),
88
+ signal
89
+ })
90
+
91
+ if (!res.ok) throw httpError(res.status, await res.text())
92
+ yield* openaiChatStream(res.body)
93
+ }
@@ -47,7 +47,7 @@ export function defaults() {
47
47
  }
48
48
 
49
49
  export function authorizeUrl() {
50
- return 'https://chat.z.ai/api/oauth/authorize'
50
+ return 'https://open.bigmodel.cn/usercenter/apikeys'
51
51
  }
52
52
 
53
53
  export async function listModels() {