diff --git a/package-lock.json b/package-lock.json index 1b0c537..c90cabd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "opencode-lmstudio", - "version": "0.2.0", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "opencode-lmstudio", - "version": "0.2.0", + "version": "0.3.0", "license": "MIT", "dependencies": { "@opencode-ai/plugin": "^1.0.166" diff --git a/src/utils/http.ts b/src/utils/http.ts new file mode 100644 index 0000000..3fbd923 --- /dev/null +++ b/src/utils/http.ts @@ -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 { + const config = getLMStudioAuth() + const headers: Record = { + 'Content-Type': 'application/json' + } + + // Only add Authorization header if API key is configured + if (config.key) { + headers['Authorization'] = `Bearer ${config.key}` + } + + return headers +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 4d701d2..f2bb9d4 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -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 @@ -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 } @@ -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 diff --git a/src/utils/lmstudio-api.ts b/src/utils/lmstudio-api.ts index 62967e7..34fcdc3 100644 --- a/src/utils/lmstudio-api.ts +++ b/src/utils/lmstudio-api.ts @@ -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" @@ -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 { try { const url = buildAPIURL(baseURL) + const headers = getHeaders() const response = await fetch(url, { method: "GET", + headers: headers, signal: AbortSignal.timeout(3000), }) return response.ok @@ -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 { 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), }) @@ -63,8 +65,10 @@ export async function discoverLMStudioModels(baseURL: string = DEFAULT_LM_STUDIO export async function fetchModelsDirect(baseURL: string = DEFAULT_LM_STUDIO_URL): Promise { try { const url = buildAPIURL(baseURL) + const headers = getHeaders() const response = await fetch(url, { method: "GET", + headers: headers, signal: AbortSignal.timeout(3000), }) if (!response.ok) { diff --git a/test/plugin.test.ts b/test/plugin.test.ts index ad05400..5a44926 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -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() + } + }) }) }) \ No newline at end of file