@link-assistant/agent 0.8.21 → 0.9.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/README.md CHANGED
@@ -20,11 +20,11 @@
20
20
 
21
21
  > This is the JavaScript/Bun implementation. See also the [Rust implementation](../rust/README.md).
22
22
 
23
- This is an MVP implementation of an OpenCode-compatible CLI agent, focused on maximum efficiency and unrestricted execution. We reproduce OpenCode's `run --format json --model opencode/grok-code` mode with:
23
+ This is an MVP implementation of an OpenCode-compatible CLI agent, focused on maximum efficiency and unrestricted execution. We reproduce OpenCode's `run --format json --model opencode/kimi-k2.5-free` mode with:
24
24
 
25
- - ✅ **JSON Input/Output**: Compatible with `opencode run --format json --model opencode/grok-code`
25
+ - ✅ **JSON Input/Output**: Compatible with `opencode run --format json --model opencode/kimi-k2.5-free`
26
26
  - ✅ **Plain Text Input**: Also accepts plain text messages (auto-converted to JSON format)
27
- - ✅ **Flexible Model Selection**: Defaults to free OpenCode Zen Grok Code Fast 1, supports [OpenCode Zen](https://opencode.ai/docs/zen/), [Claude OAuth](../docs/claude-oauth.md), [Groq](../docs/groq.md), and [OpenRouter](../docs/openrouter.md) providers
27
+ - ✅ **Flexible Model Selection**: Defaults to free OpenCode Zen Kimi K2.5, supports [OpenCode Zen](https://opencode.ai/docs/zen/), [Claude OAuth](../docs/claude-oauth.md), [Groq](../docs/groq.md), and [OpenRouter](../docs/openrouter.md) providers
28
28
  - ✅ **No Restrictions**: Fully unrestricted file system and command execution access (no sandbox)
29
29
  - ✅ **Minimal Footprint**: Built with Bun.sh for maximum efficiency
30
30
  - ✅ **Full Tool Support**: 13 tools including websearch, codesearch, batch - all enabled by default
@@ -169,7 +169,7 @@ echo '{"message":"hi"}' | agent
169
169
  **With custom model:**
170
170
 
171
171
  ```bash
172
- echo "hi" | agent --model opencode/grok-code
172
+ echo "hi" | agent --model opencode/kimi-k2.5-free
173
173
  ```
174
174
 
175
175
  ### More Examples
@@ -190,12 +190,14 @@ echo '{"message":"run command","tools":[{"name":"bash","params":{"command":"ls -
190
190
  **Using different models:**
191
191
 
192
192
  ```bash
193
- # Default model (free Grok Code Fast 1)
193
+ # Default model (free Kimi K2.5)
194
194
  echo "hi" | agent
195
195
 
196
- # Other free models
197
- echo "hi" | agent --model opencode/big-pickle
196
+ # Other free models (in order of recommendation)
197
+ echo "hi" | agent --model opencode/minimax-m2.1-free
198
198
  echo "hi" | agent --model opencode/gpt-5-nano
199
+ echo "hi" | agent --model opencode/glm-4.7-free
200
+ echo "hi" | agent --model opencode/big-pickle
199
201
 
200
202
  # Premium models (OpenCode Zen subscription)
201
203
  echo "hi" | agent --model opencode/sonnet # Claude Sonnet 4.5
@@ -279,7 +281,7 @@ agent [options]
279
281
 
280
282
  Options:
281
283
  --model Model to use in format providerID/modelID
282
- Default: opencode/grok-code
284
+ Default: opencode/kimi-k2.5-free
283
285
  --json-standard JSON output format standard
284
286
  Choices: "opencode" (default), "claude" (experimental)
285
287
  --use-existing-claude-oauth Use existing Claude OAuth credentials
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/agent",
3
- "version": "0.8.21",
3
+ "version": "0.9.0",
4
4
  "description": "A minimal, public domain AI CLI agent compatible with OpenCode's JSON interface. Bun-only runtime.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -1277,9 +1277,40 @@ const GooglePlugin: AuthPlugin = {
1277
1277
  *
1278
1278
  * @see https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/code_assist/server.ts
1279
1279
  * @see https://github.com/link-assistant/agent/issues/100
1280
+ * @see https://github.com/link-assistant/agent/issues/102
1280
1281
  */
1281
- const CLOUD_CODE_ENDPOINT = 'https://cloudcode-pa.googleapis.com';
1282
- const CLOUD_CODE_API_VERSION = 'v1internal';
1282
+ const CLOUD_CODE_ENDPOINT =
1283
+ process.env['CODE_ASSIST_ENDPOINT'] ||
1284
+ 'https://cloudcode-pa.googleapis.com';
1285
+ const CLOUD_CODE_API_VERSION =
1286
+ process.env['CODE_ASSIST_API_VERSION'] || 'v1internal';
1287
+
1288
+ /**
1289
+ * Synthetic thought signature used for Gemini 3+ function calls.
1290
+ * The Cloud Code API requires function call parts in the active loop
1291
+ * to have a thoughtSignature. This bypass value is accepted by the API.
1292
+ *
1293
+ * @see https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/core/geminiChat.ts
1294
+ */
1295
+ const SYNTHETIC_THOUGHT_SIGNATURE = 'skip_thought_signature_validator';
1296
+
1297
+ /**
1298
+ * Retry configuration for transient Cloud Code API errors.
1299
+ * Matches patterns from Gemini CLI and opencode-gemini-auth plugin.
1300
+ */
1301
+ const MAX_RETRIES = 2;
1302
+ const RETRY_BASE_DELAY_MS = 800;
1303
+ const RETRY_MAX_DELAY_MS = 8000;
1304
+
1305
+ /**
1306
+ * Cached project context from Cloud Code API onboarding.
1307
+ * Persists across requests to avoid repeated loadCodeAssist calls.
1308
+ * Keyed by refresh token to invalidate when auth changes.
1309
+ */
1310
+ let cachedProjectContext: {
1311
+ projectId?: string;
1312
+ refreshToken: string;
1313
+ } | null = null;
1283
1314
 
1284
1315
  log.debug(() => ({
1285
1316
  message: 'google oauth loader initialized',
@@ -1287,13 +1318,187 @@ const GooglePlugin: AuthPlugin = {
1287
1318
  apiVersion: CLOUD_CODE_API_VERSION,
1288
1319
  }));
1289
1320
 
1321
+ /**
1322
+ * Ensure project context is available for Cloud Code API requests.
1323
+ * Calls loadCodeAssist to check user tier and onboards if necessary.
1324
+ * Results are cached to avoid repeated API calls.
1325
+ *
1326
+ * @see https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/code_assist/setup.ts
1327
+ */
1328
+ const ensureProjectContext = async (
1329
+ accessToken: string,
1330
+ refreshToken: string
1331
+ ): Promise<string | undefined> => {
1332
+ // Check for explicit project ID from environment
1333
+ const envProjectId =
1334
+ process.env['GOOGLE_CLOUD_PROJECT'] ||
1335
+ process.env['GOOGLE_CLOUD_PROJECT_ID'];
1336
+ if (envProjectId) {
1337
+ return envProjectId;
1338
+ }
1339
+
1340
+ // Return cached context if still valid (same refresh token)
1341
+ if (
1342
+ cachedProjectContext &&
1343
+ cachedProjectContext.refreshToken === refreshToken
1344
+ ) {
1345
+ return cachedProjectContext.projectId;
1346
+ }
1347
+
1348
+ // Call loadCodeAssist to discover project and tier
1349
+ try {
1350
+ const loadUrl = `${CLOUD_CODE_ENDPOINT}/${CLOUD_CODE_API_VERSION}:loadCodeAssist`;
1351
+ const loadRes = await fetch(loadUrl, {
1352
+ method: 'POST',
1353
+ headers: {
1354
+ 'Content-Type': 'application/json',
1355
+ Authorization: `Bearer ${accessToken}`,
1356
+ },
1357
+ body: JSON.stringify({
1358
+ metadata: {
1359
+ ideType: 'IDE_UNSPECIFIED',
1360
+ platform: 'PLATFORM_UNSPECIFIED',
1361
+ pluginType: 'GEMINI',
1362
+ },
1363
+ }),
1364
+ });
1365
+
1366
+ if (!loadRes.ok) {
1367
+ const errorText = await loadRes.text().catch(() => 'unknown');
1368
+ log.warn(() => ({
1369
+ message: 'loadCodeAssist failed, proceeding without project',
1370
+ status: loadRes.status,
1371
+ error: errorText.substring(0, 200),
1372
+ }));
1373
+ // Cache empty result to avoid retrying on every request
1374
+ cachedProjectContext = { refreshToken };
1375
+ return undefined;
1376
+ }
1377
+
1378
+ const loadData = await loadRes.json();
1379
+ log.debug(() => ({
1380
+ message: 'loadCodeAssist response',
1381
+ hasCurrentTier: !!loadData.currentTier,
1382
+ tierId: loadData.currentTier?.id,
1383
+ hasProject: !!loadData.cloudaicompanionProject,
1384
+ }));
1385
+
1386
+ // If user already has a tier and project, use it
1387
+ if (loadData.currentTier && loadData.cloudaicompanionProject) {
1388
+ cachedProjectContext = {
1389
+ projectId: loadData.cloudaicompanionProject,
1390
+ refreshToken,
1391
+ };
1392
+ log.info(() => ({
1393
+ message: 'user already onboarded',
1394
+ tier: loadData.currentTier.id,
1395
+ projectId: loadData.cloudaicompanionProject,
1396
+ }));
1397
+ return loadData.cloudaicompanionProject;
1398
+ }
1399
+
1400
+ // If user has a tier but no project, and paidTier exists
1401
+ if (loadData.currentTier) {
1402
+ cachedProjectContext = { refreshToken };
1403
+ return undefined;
1404
+ }
1405
+
1406
+ // User needs onboarding - find default tier
1407
+ let targetTierId = 'free-tier';
1408
+ for (const tier of loadData.allowedTiers || []) {
1409
+ if (tier.isDefault) {
1410
+ targetTierId = tier.id;
1411
+ break;
1412
+ }
1413
+ }
1414
+
1415
+ log.info(() => ({
1416
+ message: 'onboarding user to tier',
1417
+ tier: targetTierId,
1418
+ }));
1419
+
1420
+ // Call onboardUser
1421
+ const onboardUrl = `${CLOUD_CODE_ENDPOINT}/${CLOUD_CODE_API_VERSION}:onboardUser`;
1422
+ const onboardReq =
1423
+ targetTierId === 'free-tier'
1424
+ ? {
1425
+ tierId: targetTierId,
1426
+ metadata: {
1427
+ ideType: 'IDE_UNSPECIFIED',
1428
+ platform: 'PLATFORM_UNSPECIFIED',
1429
+ pluginType: 'GEMINI',
1430
+ },
1431
+ }
1432
+ : {
1433
+ tierId: targetTierId,
1434
+ cloudaicompanionProject: envProjectId,
1435
+ metadata: {
1436
+ ideType: 'IDE_UNSPECIFIED',
1437
+ platform: 'PLATFORM_UNSPECIFIED',
1438
+ pluginType: 'GEMINI',
1439
+ duetProject: envProjectId,
1440
+ },
1441
+ };
1442
+
1443
+ let lroRes = await fetch(onboardUrl, {
1444
+ method: 'POST',
1445
+ headers: {
1446
+ 'Content-Type': 'application/json',
1447
+ Authorization: `Bearer ${accessToken}`,
1448
+ },
1449
+ body: JSON.stringify(onboardReq),
1450
+ }).then((r) => r.json());
1451
+
1452
+ // Poll until onboarding completes (max 10 attempts)
1453
+ let attempts = 0;
1454
+ while (!lroRes.done && attempts < 10) {
1455
+ await new Promise((resolve) => setTimeout(resolve, 5000));
1456
+ if (lroRes.name) {
1457
+ // Poll operation status
1458
+ const opUrl = `${CLOUD_CODE_ENDPOINT}/${CLOUD_CODE_API_VERSION}/${lroRes.name}`;
1459
+ lroRes = await fetch(opUrl, {
1460
+ headers: { Authorization: `Bearer ${accessToken}` },
1461
+ }).then((r) => r.json());
1462
+ } else {
1463
+ lroRes = await fetch(onboardUrl, {
1464
+ method: 'POST',
1465
+ headers: {
1466
+ 'Content-Type': 'application/json',
1467
+ Authorization: `Bearer ${accessToken}`,
1468
+ },
1469
+ body: JSON.stringify(onboardReq),
1470
+ }).then((r) => r.json());
1471
+ }
1472
+ attempts++;
1473
+ }
1474
+
1475
+ const projectId =
1476
+ lroRes.response?.cloudaicompanionProject?.id || undefined;
1477
+ cachedProjectContext = { projectId, refreshToken };
1478
+
1479
+ log.info(() => ({
1480
+ message: 'user onboarding complete',
1481
+ projectId,
1482
+ attempts,
1483
+ }));
1484
+
1485
+ return projectId;
1486
+ } catch (error) {
1487
+ log.warn(() => ({
1488
+ message: 'project context setup failed, proceeding without project',
1489
+ error: String(error),
1490
+ }));
1491
+ cachedProjectContext = { refreshToken };
1492
+ return undefined;
1493
+ }
1494
+ };
1495
+
1290
1496
  /**
1291
1497
  * Check if we have a fallback API key available.
1292
1498
  * This allows trying API key authentication if Cloud Code API fails.
1293
1499
  * See: https://github.com/link-assistant/agent/issues/100
1294
1500
  */
1295
1501
  const getFallbackApiKey = (): string | undefined => {
1296
- // Check for API key in environment variables
1297
1502
  const envKey =
1298
1503
  process.env['GOOGLE_GENERATIVE_AI_API_KEY'] ||
1299
1504
  process.env['GEMINI_API_KEY'];
@@ -1309,19 +1514,11 @@ const GooglePlugin: AuthPlugin = {
1309
1514
  return envKey;
1310
1515
  }
1311
1516
 
1312
- log.debug(() => ({
1313
- message: 'no fallback api key available',
1314
- hint: 'Set GOOGLE_GENERATIVE_AI_API_KEY or GEMINI_API_KEY for fallback',
1315
- }));
1316
-
1317
- // Check for API key in auth storage (async, so we need to handle this differently)
1318
- // For now, we only support env var fallback synchronously
1319
1517
  return undefined;
1320
1518
  };
1321
1519
 
1322
1520
  /**
1323
1521
  * Detect if an error is a scope-related authentication error.
1324
- * This is triggered when OAuth token doesn't have the required scopes.
1325
1522
  */
1326
1523
  const isScopeError = (response: Response): boolean => {
1327
1524
  if (response.status !== 403) return false;
@@ -1341,15 +1538,44 @@ const GooglePlugin: AuthPlugin = {
1341
1538
  return isScope;
1342
1539
  };
1343
1540
 
1541
+ /**
1542
+ * Check if a response status is retryable (transient error).
1543
+ */
1544
+ const isRetryableStatus = (status: number): boolean => {
1545
+ return status === 429 || status === 503;
1546
+ };
1547
+
1548
+ /**
1549
+ * Extract retry delay from response headers or error body.
1550
+ */
1551
+ const getRetryDelay = (response: Response, attempt: number): number => {
1552
+ // Check Retry-After header
1553
+ const retryAfter = response.headers.get('retry-after');
1554
+ if (retryAfter) {
1555
+ const seconds = parseInt(retryAfter, 10);
1556
+ if (!isNaN(seconds)) return seconds * 1000;
1557
+ }
1558
+
1559
+ // Check retry-after-ms header
1560
+ const retryAfterMs = response.headers.get('retry-after-ms');
1561
+ if (retryAfterMs) {
1562
+ const ms = parseInt(retryAfterMs, 10);
1563
+ if (!isNaN(ms)) return ms;
1564
+ }
1565
+
1566
+ // Exponential backoff
1567
+ const delay = Math.min(
1568
+ RETRY_BASE_DELAY_MS * Math.pow(2, attempt),
1569
+ RETRY_MAX_DELAY_MS
1570
+ );
1571
+ return delay;
1572
+ };
1573
+
1344
1574
  /**
1345
1575
  * Transform a Generative Language API URL to Cloud Code API URL
1346
1576
  *
1347
1577
  * Input: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent
1348
1578
  * Output: https://cloudcode-pa.googleapis.com/v1internal:generateContent
1349
- *
1350
- * The Cloud Code API uses a different URL structure:
1351
- * - Model is passed in request body, not URL path
1352
- * - Method is directly after version: /v1internal:methodName
1353
1579
  */
1354
1580
  const transformToCloudCodeUrl = (url: string): string | null => {
1355
1581
  try {
@@ -1360,18 +1586,16 @@ const GooglePlugin: AuthPlugin = {
1360
1586
  'url is not generativelanguage api, skipping cloud code transform',
1361
1587
  hostname: parsed.hostname,
1362
1588
  }));
1363
- return null; // Not a Generative Language API URL
1589
+ return null;
1364
1590
  }
1365
1591
 
1366
- // Extract the method from the path
1367
- // Path format: /v1beta/models/gemini-2.0-flash:generateContent
1368
1592
  const pathMatch = parsed.pathname.match(/:(\w+)$/);
1369
1593
  if (!pathMatch) {
1370
1594
  log.debug(() => ({
1371
1595
  message: 'could not extract method from url path',
1372
1596
  pathname: parsed.pathname,
1373
1597
  }));
1374
- return null; // Can't determine method
1598
+ return null;
1375
1599
  }
1376
1600
 
1377
1601
  const method = pathMatch[1];
@@ -1395,6 +1619,14 @@ const GooglePlugin: AuthPlugin = {
1395
1619
  }
1396
1620
  };
1397
1621
 
1622
+ /**
1623
+ * Check if a URL is for a streaming request.
1624
+ * The AI SDK appends alt=sse for streaming or uses streamGenerateContent method.
1625
+ */
1626
+ const isStreamingRequest = (url: string): boolean => {
1627
+ return url.includes('streamGenerateContent') || url.includes('alt=sse');
1628
+ };
1629
+
1398
1630
  /**
1399
1631
  * Extract model name from Generative Language API URL
1400
1632
  *
@@ -1404,7 +1636,6 @@ const GooglePlugin: AuthPlugin = {
1404
1636
  const extractModelFromUrl = (url: string): string | null => {
1405
1637
  try {
1406
1638
  const parsed = new URL(url);
1407
- // Path format: /v1beta/models/gemini-2.0-flash:generateContent
1408
1639
  const pathMatch = parsed.pathname.match(/\/models\/([^:]+):/);
1409
1640
  const model = pathMatch ? pathMatch[1] : null;
1410
1641
 
@@ -1425,6 +1656,69 @@ const GooglePlugin: AuthPlugin = {
1425
1656
  }
1426
1657
  };
1427
1658
 
1659
+ /**
1660
+ * Inject thoughtSignature into function call parts for Gemini 3+ models.
1661
+ *
1662
+ * The Cloud Code API requires function call parts in model turns within
1663
+ * the active loop to have a `thoughtSignature` property. Without this,
1664
+ * requests with function calls will fail with 400 errors.
1665
+ *
1666
+ * @see https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/core/geminiChat.ts
1667
+ * @see https://github.com/sst/opencode/issues/4832
1668
+ */
1669
+ const injectThoughtSignatures = (request: any): any => {
1670
+ if (!request?.contents || !Array.isArray(request.contents)) {
1671
+ return request;
1672
+ }
1673
+
1674
+ // Find the start of the active loop (last user turn with text)
1675
+ let activeLoopStartIndex = -1;
1676
+ for (let i = request.contents.length - 1; i >= 0; i--) {
1677
+ const content = request.contents[i];
1678
+ if (
1679
+ content.role === 'user' &&
1680
+ content.parts?.some((p: any) => p.text)
1681
+ ) {
1682
+ activeLoopStartIndex = i;
1683
+ break;
1684
+ }
1685
+ }
1686
+
1687
+ if (activeLoopStartIndex === -1) {
1688
+ return request;
1689
+ }
1690
+
1691
+ // Inject thoughtSignature into the first functionCall in each model turn
1692
+ const newContents = [...request.contents];
1693
+ let modified = false;
1694
+ for (let i = activeLoopStartIndex; i < newContents.length; i++) {
1695
+ const content = newContents[i];
1696
+ if (content.role === 'model' && content.parts) {
1697
+ const newParts = [...content.parts];
1698
+ for (let j = 0; j < newParts.length; j++) {
1699
+ const part = newParts[j];
1700
+ if (part.functionCall && !part.thoughtSignature) {
1701
+ newParts[j] = {
1702
+ ...part,
1703
+ thoughtSignature: SYNTHETIC_THOUGHT_SIGNATURE,
1704
+ };
1705
+ newContents[i] = { ...content, parts: newParts };
1706
+ modified = true;
1707
+ break; // Only the first functionCall in each turn
1708
+ }
1709
+ }
1710
+ }
1711
+ }
1712
+
1713
+ if (modified) {
1714
+ log.debug(() => ({
1715
+ message: 'injected thoughtSignature into function call parts',
1716
+ }));
1717
+ }
1718
+
1719
+ return { ...request, contents: newContents };
1720
+ };
1721
+
1428
1722
  /**
1429
1723
  * Transform request body for Cloud Code API
1430
1724
  *
@@ -1432,29 +1726,30 @@ const GooglePlugin: AuthPlugin = {
1432
1726
  * {
1433
1727
  * model: "gemini-2.0-flash",
1434
1728
  * project: "optional-project-id",
1435
- * user_prompt_id: "optional-prompt-id",
1436
1729
  * request: { contents: [...], generationConfig: {...}, ... }
1437
1730
  * }
1438
- *
1439
- * The standard AI SDK sends:
1440
- * { contents: [...], generationConfig: {...}, ... }
1441
1731
  */
1442
- const transformRequestBody = (body: string, model: string): string => {
1732
+ const transformRequestBody = (
1733
+ body: string,
1734
+ model: string,
1735
+ projectId?: string
1736
+ ): string => {
1443
1737
  try {
1444
- const parsed = JSON.parse(body);
1738
+ let parsed = JSON.parse(body);
1445
1739
 
1446
- // Get project ID from environment if available
1447
- const projectId =
1448
- process.env['GOOGLE_CLOUD_PROJECT'] ||
1449
- process.env['GOOGLE_CLOUD_PROJECT_ID'];
1740
+ // Inject thoughtSignature for function calls
1741
+ parsed = injectThoughtSignatures(parsed);
1450
1742
 
1451
1743
  // Wrap in Cloud Code API format
1452
- const cloudCodeRequest = {
1744
+ const cloudCodeRequest: Record<string, unknown> = {
1453
1745
  model,
1454
- ...(projectId && { project: projectId }),
1455
1746
  request: parsed,
1456
1747
  };
1457
1748
 
1749
+ if (projectId) {
1750
+ cloudCodeRequest.project = projectId;
1751
+ }
1752
+
1458
1753
  log.debug(() => ({
1459
1754
  message: 'transformed request body for cloud code api',
1460
1755
  model,
@@ -1469,7 +1764,7 @@ const GooglePlugin: AuthPlugin = {
1469
1764
  message: 'failed to transform request body, using original',
1470
1765
  error: String(error),
1471
1766
  }));
1472
- return body; // Return original if parsing fails
1767
+ return body;
1473
1768
  }
1474
1769
  };
1475
1770
 
@@ -1495,10 +1790,7 @@ const GooglePlugin: AuthPlugin = {
1495
1790
  isStreaming,
1496
1791
  }));
1497
1792
 
1498
- // For streaming responses, we need to transform each chunk
1499
1793
  if (isStreaming) {
1500
- // The Cloud Code API returns SSE with data: { response: {...} } format
1501
- // We need to transform each chunk to unwrap the response
1502
1794
  const reader = response.body?.getReader();
1503
1795
  if (!reader) {
1504
1796
  log.debug(() => ({
@@ -1528,12 +1820,10 @@ const GooglePlugin: AuthPlugin = {
1528
1820
  const text = decoder.decode(value, { stream: true });
1529
1821
  chunkCount++;
1530
1822
 
1531
- // Split by SSE event boundaries
1532
1823
  const events = text.split('\n\n');
1533
1824
  for (const event of events) {
1534
1825
  if (!event.trim()) continue;
1535
1826
 
1536
- // Check if this is a data line
1537
1827
  if (event.startsWith('data: ')) {
1538
1828
  try {
1539
1829
  const jsonStr = event.slice(6).trim();
@@ -1543,8 +1833,6 @@ const GooglePlugin: AuthPlugin = {
1543
1833
  }
1544
1834
 
1545
1835
  const parsed = JSON.parse(jsonStr);
1546
-
1547
- // Unwrap Cloud Code response format if present
1548
1836
  const unwrapped = parsed.response || parsed;
1549
1837
  controller.enqueue(
1550
1838
  encoder.encode(
@@ -1552,11 +1840,9 @@ const GooglePlugin: AuthPlugin = {
1552
1840
  )
1553
1841
  );
1554
1842
  } catch {
1555
- // If parsing fails, pass through as-is
1556
1843
  controller.enqueue(encoder.encode(event + '\n\n'));
1557
1844
  }
1558
1845
  } else {
1559
- // Non-data lines (like event type), pass through
1560
1846
  controller.enqueue(encoder.encode(event + '\n\n'));
1561
1847
  }
1562
1848
  }
@@ -1634,6 +1920,10 @@ const GooglePlugin: AuthPlugin = {
1634
1920
  ? 'no access token'
1635
1921
  : 'token expiring soon',
1636
1922
  }));
1923
+
1924
+ // Invalidate project cache when token changes
1925
+ cachedProjectContext = null;
1926
+
1637
1927
  const response = await fetch(GOOGLE_TOKEN_URL, {
1638
1928
  method: 'POST',
1639
1929
  headers: {
@@ -1698,50 +1988,94 @@ const GooglePlugin: AuthPlugin = {
1698
1988
 
1699
1989
  // If this is a Generative Language API request, route through Cloud Code API
1700
1990
  if (cloudCodeUrl && model) {
1991
+ // Ensure project context is available (onboard if needed)
1992
+ const projectId = await ensureProjectContext(
1993
+ currentAuth.access,
1994
+ currentAuth.refresh
1995
+ );
1996
+
1701
1997
  log.info(() => ({
1702
1998
  message: 'routing google oauth request through cloud code api',
1703
1999
  originalUrl: originalUrl.substring(0, 100) + '...',
1704
2000
  cloudCodeUrl,
1705
2001
  model,
2002
+ projectId: projectId || '(none)',
1706
2003
  }));
1707
2004
 
1708
2005
  // Transform request body to Cloud Code format
1709
2006
  let body = init?.body;
1710
2007
  if (typeof body === 'string') {
1711
- body = transformRequestBody(body, model);
2008
+ body = transformRequestBody(body, model, projectId);
1712
2009
  }
1713
2010
 
1714
- // Make request to Cloud Code API with Bearer token
2011
+ // Build Cloud Code API URL with alt=sse for streaming
2012
+ let finalCloudCodeUrl = cloudCodeUrl;
2013
+ if (isStreamingRequest(originalUrl)) {
2014
+ const separator = finalCloudCodeUrl.includes('?') ? '&' : '?';
2015
+ finalCloudCodeUrl = `${finalCloudCodeUrl}${separator}alt=sse`;
2016
+ }
2017
+
2018
+ // Make request to Cloud Code API with Bearer token and retry logic
1715
2019
  const headers: Record<string, string> = {
1716
2020
  ...(init?.headers as Record<string, string>),
1717
2021
  Authorization: `Bearer ${currentAuth.access}`,
1718
- 'x-goog-api-client': 'agent/0.6.3',
2022
+ 'x-goog-api-client': `agent/${process.env['npm_package_version'] || '0.7.0'}`,
1719
2023
  };
1720
- // Remove any API key header if present since we're using OAuth
1721
2024
  delete headers['x-goog-api-key'];
1722
2025
 
1723
2026
  log.debug(() => ({
1724
2027
  message: 'sending request to cloud code api',
1725
- url: cloudCodeUrl,
2028
+ url: finalCloudCodeUrl,
1726
2029
  hasBody: !!body,
1727
2030
  }));
1728
2031
 
1729
- const cloudCodeResponse = await fetch(cloudCodeUrl, {
1730
- ...init,
1731
- body,
1732
- headers,
1733
- });
2032
+ // Retry loop for transient errors
2033
+ let lastResponse: Response | null = null;
2034
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
2035
+ if (attempt > 0) {
2036
+ const delay = getRetryDelay(lastResponse!, attempt - 1);
2037
+ log.info(() => ({
2038
+ message: 'retrying cloud code api request',
2039
+ attempt,
2040
+ delayMs: delay,
2041
+ previousStatus: lastResponse?.status,
2042
+ }));
2043
+ await new Promise((resolve) => setTimeout(resolve, delay));
2044
+ }
1734
2045
 
1735
- log.debug(() => ({
1736
- message: 'cloud code api response received',
1737
- status: cloudCodeResponse.status,
1738
- statusText: cloudCodeResponse.statusText,
1739
- contentType: cloudCodeResponse.headers.get('content-type'),
1740
- }));
2046
+ const cloudCodeResponse = await fetch(finalCloudCodeUrl, {
2047
+ ...init,
2048
+ body,
2049
+ headers,
2050
+ });
1741
2051
 
1742
- // Check for errors and handle fallback
1743
- if (!cloudCodeResponse.ok) {
1744
- // Try to get error details for logging
2052
+ log.debug(() => ({
2053
+ message: 'cloud code api response received',
2054
+ status: cloudCodeResponse.status,
2055
+ statusText: cloudCodeResponse.statusText,
2056
+ contentType: cloudCodeResponse.headers.get('content-type'),
2057
+ attempt,
2058
+ }));
2059
+
2060
+ // Success - transform and return
2061
+ if (cloudCodeResponse.ok) {
2062
+ log.debug(() => ({
2063
+ message:
2064
+ 'cloud code api request successful, transforming response',
2065
+ }));
2066
+ return transformResponseBody(cloudCodeResponse);
2067
+ }
2068
+
2069
+ // Retryable error
2070
+ if (
2071
+ isRetryableStatus(cloudCodeResponse.status) &&
2072
+ attempt < MAX_RETRIES
2073
+ ) {
2074
+ lastResponse = cloudCodeResponse;
2075
+ continue;
2076
+ }
2077
+
2078
+ // Non-retryable error or max retries reached
1745
2079
  const errorBody = await cloudCodeResponse
1746
2080
  .clone()
1747
2081
  .text()
@@ -1751,6 +2085,7 @@ const GooglePlugin: AuthPlugin = {
1751
2085
  status: cloudCodeResponse.status,
1752
2086
  statusText: cloudCodeResponse.statusText,
1753
2087
  errorBody: errorBody.substring(0, 500),
2088
+ attempt,
1754
2089
  }));
1755
2090
 
1756
2091
  const fallbackApiKey = getFallbackApiKey();
@@ -1762,18 +2097,12 @@ const GooglePlugin: AuthPlugin = {
1762
2097
  fallbackTarget: originalUrl.substring(0, 100),
1763
2098
  }));
1764
2099
 
1765
- // Fall back to standard API with API key
1766
2100
  const apiKeyHeaders: Record<string, string> = {
1767
2101
  ...(init?.headers as Record<string, string>),
1768
2102
  'x-goog-api-key': fallbackApiKey,
1769
2103
  };
1770
2104
  delete apiKeyHeaders['Authorization'];
1771
2105
 
1772
- log.debug(() => ({
1773
- message: 'sending fallback request with api key',
1774
- url: originalUrl.substring(0, 100),
1775
- }));
1776
-
1777
2106
  return fetch(originalUrl, {
1778
2107
  ...init,
1779
2108
  headers: apiKeyHeaders,
@@ -1786,16 +2115,11 @@ const GooglePlugin: AuthPlugin = {
1786
2115
  hint: 'Set GOOGLE_GENERATIVE_AI_API_KEY or GEMINI_API_KEY environment variable for fallback',
1787
2116
  }));
1788
2117
 
1789
- // No fallback available, return the error response
1790
2118
  return cloudCodeResponse;
1791
2119
  }
1792
2120
 
1793
- log.debug(() => ({
1794
- message: 'cloud code api request successful, transforming response',
1795
- }));
1796
-
1797
- // Transform response back to standard format
1798
- return transformResponseBody(cloudCodeResponse);
2121
+ // Should not reach here, but return last response as safety net
2122
+ return lastResponse!;
1799
2123
  }
1800
2124
 
1801
2125
  // Not a Generative Language API request, use standard OAuth flow
package/src/index.js CHANGED
@@ -147,7 +147,7 @@ async function parseModelConfig(argv) {
147
147
  // Parse model argument (handle model IDs with slashes like groq/qwen/qwen3-32b)
148
148
  const modelParts = argv.model.split('/');
149
149
  let providerID = modelParts[0] || 'opencode';
150
- let modelID = modelParts.slice(1).join('/') || 'grok-code';
150
+ let modelID = modelParts.slice(1).join('/') || 'kimi-k2.5-free';
151
151
 
152
152
  // Handle --use-existing-claude-oauth option
153
153
  // This reads OAuth credentials from ~/.claude/.credentials.json (Claude Code CLI)
@@ -175,7 +175,7 @@ async function parseModelConfig(argv) {
175
175
 
176
176
  // If user specified a model, use it with claude-oauth provider
177
177
  // If not, use claude-oauth/claude-sonnet-4-5 as default
178
- if (providerID === 'opencode' && modelID === 'grok-code') {
178
+ if (providerID === 'opencode' && modelID === 'kimi-k2.5-free') {
179
179
  providerID = 'claude-oauth';
180
180
  modelID = 'claude-sonnet-4-5';
181
181
  } else if (!['claude-oauth', 'anthropic'].includes(providerID)) {
@@ -575,7 +575,7 @@ async function main() {
575
575
  .option('model', {
576
576
  type: 'string',
577
577
  description: 'Model to use in format providerID/modelID',
578
- default: 'opencode/grok-code',
578
+ default: 'opencode/kimi-k2.5-free',
579
579
  })
580
580
  .option('json-standard', {
581
581
  type: 'string',
@@ -19,8 +19,11 @@ import { Auth } from '../auth';
19
19
  const log = Log.create({ service: 'google-cloudcode' });
20
20
 
21
21
  // Cloud Code API endpoints (from gemini-cli)
22
- const CODE_ASSIST_ENDPOINT = 'https://cloudcode-pa.googleapis.com';
23
- const CODE_ASSIST_API_VERSION = 'v1internal';
22
+ // Configurable via environment variables for testing or alternative endpoints
23
+ const CODE_ASSIST_ENDPOINT =
24
+ process.env['CODE_ASSIST_ENDPOINT'] || 'https://cloudcode-pa.googleapis.com';
25
+ const CODE_ASSIST_API_VERSION =
26
+ process.env['CODE_ASSIST_API_VERSION'] || 'v1internal';
24
27
 
25
28
  // Google OAuth endpoints
26
29
  const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
@@ -216,14 +219,16 @@ export class CloudCodeClient {
216
219
  ): Promise<T | Response> {
217
220
  await this.ensureValidToken();
218
221
 
219
- const url = `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:${method}`;
222
+ // Add alt=sse query param for streaming requests (matches Gemini CLI behavior)
223
+ const baseUrl = `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:${method}`;
224
+ const url = options.stream ? `${baseUrl}?alt=sse` : baseUrl;
220
225
 
221
226
  const response = await fetch(url, {
222
227
  method: 'POST',
223
228
  headers: {
224
229
  'Content-Type': 'application/json',
225
230
  Authorization: `Bearer ${this.accessToken}`,
226
- 'x-goog-api-client': 'agent/0.6.3',
231
+ 'x-goog-api-client': `agent/${process.env['npm_package_version'] || '0.7.0'}`,
227
232
  },
228
233
  body: JSON.stringify(body),
229
234
  });
@@ -1005,7 +1005,13 @@ export namespace Provider {
1005
1005
  priority = priority.filter((m) => m !== 'claude-haiku-4.5');
1006
1006
  }
1007
1007
  if (providerID === 'opencode' || providerID === 'local') {
1008
- priority = ['gpt-5-nano'];
1008
+ priority = [
1009
+ 'kimi-k2.5-free',
1010
+ 'minimax-m2.1-free',
1011
+ 'gpt-5-nano',
1012
+ 'glm-4.7-free',
1013
+ 'big-pickle',
1014
+ ];
1009
1015
  }
1010
1016
  for (const item of priority) {
1011
1017
  for (const model of Object.keys(provider.info.models)) {
@@ -1015,10 +1021,13 @@ export namespace Provider {
1015
1021
  }
1016
1022
 
1017
1023
  const priority = [
1018
- 'grok-code',
1024
+ 'kimi-k2.5-free',
1025
+ 'minimax-m2.1-free',
1026
+ 'gpt-5-nano',
1027
+ 'glm-4.7-free',
1028
+ 'big-pickle',
1019
1029
  'gpt-5',
1020
1030
  'claude-sonnet-4',
1021
- 'big-pickle',
1022
1031
  'gemini-3-pro',
1023
1032
  ];
1024
1033
  export function sort(models: ModelsDev.Model[]) {
package/src/tool/task.ts CHANGED
@@ -99,7 +99,7 @@ export const TaskTool = Tool.define('task', async () => {
99
99
 
100
100
  const model = agent.model ??
101
101
  parentModel ?? {
102
- modelID: 'grok-code',
102
+ modelID: 'kimi-k2.5-free',
103
103
  providerID: 'opencode',
104
104
  };
105
105