Skip to content
Merged
317 changes: 317 additions & 0 deletions docs/case-studies/issue-171/README.md

Large diffs are not rendered by default.

851 changes: 851 additions & 0 deletions docs/case-studies/issue-171/original-failure-log.txt

Large diffs are not rendered by default.

65 changes: 65 additions & 0 deletions experiments/issue-171/debug-argv.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env bun
/**
* Debug script to check argv.model parsing
* Issue: https://github.com/link-assistant/agent/issues/171
*/

import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';

console.log('=== DEBUG ARGV ===');
console.log('process.argv:', process.argv);
console.log('hideBin(process.argv):', hideBin(process.argv));
console.log();

const yargsInstance = yargs(hideBin(process.argv))
.scriptName('agent')
.command({
command: '$0',
describe: 'Run agent',
builder: (yargs) =>
yargs.option('model', {
type: 'string',
description: 'Model to use in format providerID/modelID',
default: 'opencode/kimi-k2.5-free',
}),
handler: async (argv) => {
console.log('In handler:');
console.log(' argv.model:', argv.model);
console.log(' typeof argv.model:', typeof argv.model);
console.log(' full argv:', JSON.stringify(argv, null, 2));

// Simulate parseModelConfig logic
const modelArg = argv.model;
console.log();
console.log('parseModelConfig simulation:');
console.log(' modelArg:', modelArg);

if (modelArg.includes('/')) {
const modelParts = modelArg.split('/');
let providerID = modelParts[0];
let modelID = modelParts.slice(1).join('/');

console.log(' modelParts:', modelParts);
console.log(' providerID:', providerID);
console.log(' modelID:', modelID);

if (!providerID || !modelID) {
console.log(' >>> FALLBACK TRIGGERED <<<');
providerID = providerID || 'opencode';
modelID = modelID || 'kimi-k2.5-free';
}

console.log();
console.log('Final result:');
console.log(' providerID:', providerID);
console.log(' modelID:', modelID);
} else {
console.log(' Model does not contain "/" - would use resolution');
}
},
})
.help();

// Parse arguments
await yargsInstance.argv;
115 changes: 115 additions & 0 deletions experiments/issue-171/test-kilo-sdk.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* Test script: Verify Kilo SDK integration with @openrouter/ai-sdk-provider
*
* Tests:
* 1. SDK loads correctly
* 2. Model creation works with correct base URL
* 3. Device auth initiation works
* 4. Model listing works (anonymous access)
*
* To test with actual API call, first authenticate:
* agent auth login (select Kilo Gateway)
* Then set: KILO_API_KEY=<your-token> bun run experiments/issue-171/test-kilo-sdk.mjs
*/

const KILO_API_BASE = 'https://api.kilo.ai';
const KILO_OPENROUTER_URL = `${KILO_API_BASE}/api/openrouter`;

console.log('=== Kilo Provider Integration Test ===\n');

// Test 1: Verify @openrouter/ai-sdk-provider loads
console.log('Test 1: Loading @openrouter/ai-sdk-provider...');
try {
const { createOpenRouter } = await import('@openrouter/ai-sdk-provider');
console.log(' OK: Package loaded successfully\n');

// Test 2: Create provider with correct base URL
console.log('Test 2: Creating Kilo provider with correct base URL...');
const apiKey = process.env.KILO_API_KEY || 'anonymous';
const provider = createOpenRouter({
baseURL: KILO_OPENROUTER_URL,
apiKey,
headers: {
'User-Agent': 'opencode-kilo-provider',
'X-KILOCODE-EDITORNAME': 'link-assistant-agent',
},
});
console.log(` OK: Provider created (apiKey: ${apiKey === 'anonymous' ? 'anonymous' : '***'})\n`);

// Test 3: Create model
console.log('Test 3: Creating model z-ai/glm-5...');
const model = provider.languageModel('z-ai/glm-5');
console.log(` OK: Model created (modelId: ${model.modelId})\n`);

// Test 4: If we have a real API key, try a completion
if (apiKey !== 'anonymous') {
console.log('Test 4: Testing completion with authenticated API key...');
const { generateText } = await import('ai');
try {
const result = await generateText({
model,
messages: [{ role: 'user', content: "Say 'hello' in one word" }],
maxTokens: 10,
});
console.log(` OK: Completion successful`);
console.log(` Response: "${result.text}"\n`);
} catch (e) {
console.log(` FAIL: ${e.message}`);
if (e.statusCode) console.log(` Status: ${e.statusCode}`);
if (e.responseBody) console.log(` Body: ${e.responseBody}\n`);
}
} else {
console.log('Test 4: Skipped (no KILO_API_KEY set, anonymous access may be rejected)\n');
}
} catch (e) {
console.log(` FAIL: ${e.message}\n`);
}

// Test 5: Device auth API
console.log('Test 5: Testing device auth initiation...');
try {
const response = await fetch(`${KILO_API_BASE}/api/device-auth/codes`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
if (response.ok) {
const data = await response.json();
console.log(` OK: Device auth initiated`);
console.log(` Code: ${data.code}`);
console.log(` URL: ${data.verificationUrl}`);
console.log(` Expires in: ${data.expiresIn}s\n`);
} else {
console.log(` FAIL: HTTP ${response.status}\n`);
}
} catch (e) {
console.log(` FAIL: ${e.message}\n`);
}

// Test 6: Models listing (anonymous access works for this)
console.log('Test 6: Testing models listing...');
try {
const response = await fetch(`${KILO_OPENROUTER_URL}/models`, {
headers: {
'Authorization': 'Bearer anonymous',
'Content-Type': 'application/json',
},
});
if (response.ok) {
const data = await response.json();
const models = data.data || [];
const glmModels = models.filter((m) => m.id.includes('glm'));
console.log(` OK: ${models.length} models available`);
console.log(` GLM models found: ${glmModels.map((m) => m.id).join(', ') || 'none'}`);
const freeModels = models.filter((m) => {
const input = parseFloat(m.pricing?.prompt || '1');
return input === 0;
});
console.log(` Free models: ${freeModels.length}\n`);
} else {
console.log(` FAIL: HTTP ${response.status}\n`);
}
} catch (e) {
console.log(` FAIL: ${e.message}\n`);
}

console.log('=== Tests Complete ===');
85 changes: 85 additions & 0 deletions experiments/issue-171/test-reproduce.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/usr/bin/env bun
/**
* Test script to reproduce issue 171
* Tests if yargs correctly parses --model when stdin is piped
*
* Run with: cat prompt.txt | bun test-reproduce.js --model kilo/glm-5-free --verbose
*/

import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';

console.log('=== REPRODUCE ISSUE 171 ===');
console.log('process.argv:', process.argv);
console.log('hideBin:', hideBin(process.argv));

const yargsInstance = yargs(hideBin(process.argv))
.scriptName('agent')
.usage('$0 [command] [options]')
.command({
command: '$0',
describe: 'Run agent in interactive or stdin mode (default)',
builder: (yargs) =>
yargs
.option('model', {
type: 'string',
description: 'Model to use in format providerID/modelID',
default: 'opencode/kimi-k2.5-free',
})
.option('verbose', {
type: 'boolean',
description: 'Enable verbose mode',
default: false,
}),
handler: async (argv) => {
console.log();
console.log('=== IN HANDLER ===');
console.log('argv.model:', argv.model);
console.log('argv.verbose:', argv.verbose);

// Simulate parseModelConfig
const modelArg = argv.model;
console.log();
console.log('parseModelConfig simulation:');
console.log(' modelArg:', modelArg);

if (modelArg.includes('/')) {
const modelParts = modelArg.split('/');
let providerID = modelParts[0];
let modelID = modelParts.slice(1).join('/');

console.log(' providerID:', providerID);
console.log(' modelID:', modelID);

if (!providerID || !modelID) {
providerID = providerID || 'opencode';
modelID = modelID || 'kimi-k2.5-free';
console.log(' FALLBACK TRIGGERED');
}

console.log();
console.log('RESULT:');
console.log(' using explicit provider/model');
console.log(' providerID:', providerID);
console.log(' modelID:', modelID);

if (providerID !== 'kilo' || modelID !== 'glm-5-free') {
console.log();
console.log('*** BUG REPRODUCED! ***');
console.log('Expected: kilo/glm-5-free');
console.log('Actual:', providerID + '/' + modelID);
}
}
},
})
.middleware(async (argv) => {
// This simulates the middleware from the actual agent
console.log();
console.log('=== IN MIDDLEWARE ===');
console.log('argv.model:', argv.model);
console.log('argv.verbose:', argv.verbose);
})
.help();

// Parse arguments
await yargsInstance.argv;
39 changes: 39 additions & 0 deletions experiments/issue-171/test-stdin-model.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/bin/bash
# Test script to verify model parsing with stdin piping
# This simulates: cat prompt.txt | agent --model kilo/glm-5-free --verbose

cd /tmp/gh-issue-solver-1771070098403/js

# Create a test prompt file
echo "Hello, how are you?" > /tmp/test_prompt.txt

echo "=== Test 1: Direct model argument ==="
echo "Running: bun run src/index.js --model kilo/glm-5-free --dry-run --verbose --prompt 'test'"
bun run src/index.js --model kilo/glm-5-free --dry-run --verbose --prompt 'test' 2>&1 | head -50

echo ""
echo "=== Test 2: Piped stdin with model argument ==="
echo "Running: cat /tmp/test_prompt.txt | bun run src/index.js --model kilo/glm-5-free --dry-run --verbose"
cat /tmp/test_prompt.txt | bun run src/index.js --model kilo/glm-5-free --dry-run --verbose --no-always-accept-stdin 2>&1 | head -50

echo ""
echo "=== Test 3: Check argv parsing ==="
# Create a minimal test to check argv parsing
cat > /tmp/test_argv.js << 'EOF'
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';

console.log('process.argv:', process.argv);
console.log('hideBin(process.argv):', hideBin(process.argv));

const argv = yargs(hideBin(process.argv))
.option('model', {
type: 'string',
default: 'opencode/kimi-k2.5-free',
})
.parse();

console.log('Parsed argv.model:', argv.model);
EOF

echo "stdin test" | bun /tmp/test_argv.js --model kilo/glm-5-free
58 changes: 58 additions & 0 deletions experiments/issue-171/test-yargs-model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env bun
/**
* Test script to verify yargs model parsing behavior
* This investigates why --model kilo/glm-5-free might be ignored
*
* Issue: https://github.com/link-assistant/agent/issues/171
*/

import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';

// Simulate the command: agent --model kilo/glm-5-free --verbose
const testArgs = ['--model', 'kilo/glm-5-free', '--verbose'];

console.log('Testing yargs model parsing...');
console.log('Test arguments:', testArgs);
console.log();

const argv = yargs(testArgs)
.option('model', {
type: 'string',
description: 'Model to use in format providerID/modelID',
default: 'opencode/kimi-k2.5-free',
})
.option('verbose', {
type: 'boolean',
default: false,
})
.parse();

console.log('Parsed argv:', JSON.stringify(argv, null, 2));
console.log();

// Now test the parseModelConfig logic
const modelArg = argv.model;
console.log('modelArg:', modelArg);
console.log('modelArg includes "/":', modelArg.includes('/'));

if (modelArg.includes('/')) {
const modelParts = modelArg.split('/');
let providerID = modelParts[0];
let modelID = modelParts.slice(1).join('/');

console.log('Before validation:');
console.log(' providerID:', providerID, '(truthy:', Boolean(providerID), ')');
console.log(' modelID:', modelID, '(truthy:', Boolean(modelID), ')');

// This is the validation logic from the original code
if (!providerID || !modelID) {
providerID = providerID || 'opencode';
modelID = modelID || 'kimi-k2.5-free';
console.log('\nValidation fallback triggered!');
}

console.log('\nFinal result:');
console.log(' providerID:', providerID);
console.log(' modelID:', modelID);
}
11 changes: 11 additions & 0 deletions js/.changeset/fix-model-routing-logging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@link-assistant/agent': minor
---

Fix Kilo provider integration: correct API endpoint, SDK, model IDs, and add device auth support (#171)

- Fix base URL from /api/gateway to /api/openrouter
- Switch SDK from @ai-sdk/openai-compatible to @openrouter/ai-sdk-provider
- Fix all model ID mappings to match actual Kilo API identifiers
- Add Kilo device auth plugin for `agent auth login`
- Add required Kilo headers (User-Agent, X-KILOCODE-EDITORNAME)
Loading