Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 46 additions & 0 deletions src/utils/http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as fs from 'fs'
import * as path from 'path'

export interface LMStudioAuthConfig {
type: 'api' | null
key: string | null
}

export function getLMStudioAuth(): LMStudioAuthConfig {
const authFile = path.join(process.env.HOME || '', '.local', 'share', 'opencode', 'auth.json')

try {
const authData = JSON.parse(fs.readFileSync(authFile, 'utf8'))
const lmstudioConfig = authData['lmstudio']

if (!lmstudioConfig) {
return { type: null, key: null }
}

if (lmstudioConfig.type !== 'api') {
return { type: null, key: null }
}

return {
type: 'api',
key: lmstudioConfig.key || null
}
} catch (error) {
console.warn(`[opencode-lmstudio] Failed to read LM Studio auth from ${authFile}`, error instanceof Error ? error.message : String(error))
return { type: null, key: null }
}
}

export function getHeaders(): Record<string, string> {
const config = getLMStudioAuth()
const headers: Record<string, string> = {
'Content-Type': 'application/json'
}

// Only add Authorization header if API key is configured
if (config.key) {
headers['Authorization'] = `Bearer ${config.key}`
}

return headers
}
21 changes: 18 additions & 3 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ModelValidationError, AutoFixSuggestion, SimilarModel } from '../types'

export { getLMStudioAuth, getHeaders } from './http'
export { formatModelName, extractModelOwner } from './format-model-name'

// Categorize models by type
Expand Down Expand Up @@ -140,11 +140,11 @@ export function categorizeError(error: any, context: { baseURL: string; modelId:
}

// Permission issues
if (errorStr.includes('401') || errorStr.includes('403') || errorStr.includes('unauthorized')) {
if (errorStr.includes('401') || errorStr.includes('403') || errorStr.includes('unauthorized') || errorStr.includes('An LM Studio API token is required') || errorStr.includes('Malformed LM Studio API token provided')) {
return {
type: 'permission',
severity: 'high',
message: `Authentication or permission issue with LM Studio. Check your configuration.`,
message: `Authentication required by LM Studio server. Please configure LM Studio API key in OpenCode auth settings.`,
canRetry: false,
autoFixAvailable: false
}
Expand Down Expand Up @@ -212,6 +212,21 @@ export function generateAutoFixSuggestions(errorCategory: ModelValidationError):
automated: false
})
break

case 'permission':
suggestions.push({
action: "Configure LM Studio API key in OpenCode",
steps: [
"1. Open an opencode session",
"2. Run the command: /connect",
"3. Find and select 'LM Studio' from the list of providers",
"4. Enter your LM Studio API key and submit (found in LM Studio under Local Server > Server Settings > Manage Tokens)",
"5. Restart the opencode session",
"6. The auth.json file will be updated automatically"
],
automated: false
})
break
}

return suggestions
Expand Down
10 changes: 7 additions & 3 deletions src/utils/lmstudio-api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { LMStudioModel, LMStudioModelsResponse } from '../types'
import { getHeaders } from './http'

const DEFAULT_LM_STUDIO_URL = "http://127.0.0.1:1234"
const LM_STUDIO_MODELS_ENDPOINT = "/v1/models"
Expand Down Expand Up @@ -26,8 +27,10 @@ export function buildAPIURL(baseURL: string, endpoint: string = LM_STUDIO_MODELS
export async function checkLMStudioHealth(baseURL: string = DEFAULT_LM_STUDIO_URL): Promise<boolean> {
try {
const url = buildAPIURL(baseURL)
const headers = getHeaders()
const response = await fetch(url, {
method: "GET",
headers: headers,
signal: AbortSignal.timeout(3000),
})
return response.ok
Expand All @@ -40,11 +43,10 @@ export async function checkLMStudioHealth(baseURL: string = DEFAULT_LM_STUDIO_UR
export async function discoverLMStudioModels(baseURL: string = DEFAULT_LM_STUDIO_URL): Promise<LMStudioModel[]> {
try {
const url = buildAPIURL(baseURL)
const headers = getHeaders()
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
headers: headers,
signal: AbortSignal.timeout(3000),
})

Expand All @@ -63,8 +65,10 @@ export async function discoverLMStudioModels(baseURL: string = DEFAULT_LM_STUDIO
export async function fetchModelsDirect(baseURL: string = DEFAULT_LM_STUDIO_URL): Promise<string[]> {
try {
const url = buildAPIURL(baseURL)
const headers = getHeaders()
const response = await fetch(url, {
method: "GET",
headers: headers,
signal: AbortSignal.timeout(3000),
})
if (!response.ok) {
Expand Down
16 changes: 16 additions & 0 deletions test/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,5 +368,21 @@ describe('LMStudio Plugin', () => {

consoleSpy.mockRestore()
})

it('includes Authorization header when auth.json has valid LM Studio API key', async () => {
// Test that authorization headers are included when auth.json has valid LM Studio config
const { getHeaders } = await import('../src/utils/http.ts')
const headers = getHeaders()

// If auth.json exists with 'type: api' and key, Authorization header should be present
if (headers['Authorization']) {
expect(headers['Authorization']).toBeDefined()
expect(headers['Authorization'].startsWith('Bearer ')).toBe(true)
} else {
// No auth configured - Content-Type should still be present
expect(headers['Content-Type']).toBe('application/json')
expect(headers['Authorization']).toBeUndefined()
}
})
})
})