-
Notifications
You must be signed in to change notification settings - Fork 450
Add lms launch XYZ
#594
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Add lms launch XYZ
#594
Changes from 12 commits
ce23c8c
bb0dbf5
f37ad72
b20ba92
092b576
ab4a4e4
f1ba673
4ca0c41
bbb4880
a38dc3c
a8bdb36
4d33fd1
7b5d417
d37632c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,3 +5,5 @@ coverage | |
| .env | ||
| .DS_Store | ||
| .turbo | ||
|
|
||
| plans/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import { writeFile } from "fs/promises"; | ||
| import { join } from "path"; | ||
| import { type LaunchContext, type ToolAdapter } from "../types.js"; | ||
|
|
||
| const COMMAND = "aider"; | ||
|
|
||
| async function writeModelMetadataFile(ctx: LaunchContext): Promise<string> { | ||
| const metadataPath = join(ctx.workDir, ".aider.model.metadata.json"); | ||
| const metadata = { | ||
| [`lm_studio/${ctx.model}`]: { | ||
| max_input_tokens: ctx.contextLength, | ||
| litellm_provider: "lm_studio", | ||
| mode: "chat", | ||
| }, | ||
| }; | ||
| await writeFile(metadataPath, JSON.stringify(metadata, null, 2), "utf-8"); | ||
| return metadataPath; | ||
| } | ||
|
|
||
| /** | ||
| * Aider, via its LM Studio-native provider path. Env for the endpoint, CLI arg for model | ||
| * selection (no env equivalent exists). Verified against | ||
| * https://aider.chat/docs/llms/openai-compat.html | ||
| */ | ||
| export const aider: ToolAdapter = { | ||
| name: "aider", | ||
| displayName: "Aider", | ||
| command: COMMAND, | ||
| install: { pip: "aider-chat", url: "https://aider.chat/docs/llms/openai-compat.html" }, | ||
| supportsContextHint: true, | ||
| async prepare(ctx) { | ||
| const env: Record<string, string> = { | ||
| LM_STUDIO_API_BASE: ctx.openaiBaseUrl, | ||
| // Must be non-empty -- aider's OpenAI-compatible client rejects an empty bearer token. | ||
| LM_STUDIO_API_KEY: ctx.apiKey !== "" ? ctx.apiKey : "lmstudio", | ||
| }; | ||
| const args = ["--model", `lm_studio/${ctx.model}`]; | ||
| if (ctx.contextLength !== undefined) { | ||
| const metadataPath = await writeModelMetadataFile(ctx); | ||
| args.push("--model-metadata-file", metadataPath); | ||
| } | ||
| return { command: COMMAND, args, env }; | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import { type ToolAdapter } from "../types.js"; | ||
|
|
||
| const COMMAND = "claude"; | ||
| const CLAUDE_FIRST_PARTY_MODEL_ID_RE = /^claude-/i; | ||
|
|
||
| /** | ||
| * Claude Code. Env-only: base URL is the bare origin (Claude Code appends `/v1/messages` itself). | ||
| * Verified against https://code.claude.com/docs/en/env-vars and https://lmstudio.ai/blog/claudecode. | ||
| */ | ||
| export const claude: ToolAdapter = { | ||
| name: "claude", | ||
| aliases: ["claude-code"], | ||
| displayName: "Claude Code", | ||
| command: COMMAND, | ||
| install: { npm: "@anthropic-ai/claude-code", url: "https://lmstudio.ai/blog/claudecode" }, | ||
| supportsContextHint: true, | ||
| async prepare(ctx) { | ||
| const env: Record<string, string> = { | ||
| ANTHROPIC_BASE_URL: ctx.origin, // NOT ctx.openaiBaseUrl -- no /v1 suffix here | ||
| ANTHROPIC_AUTH_TOKEN: ctx.apiKey, | ||
| ANTHROPIC_MODEL: ctx.model, | ||
| // Pin all four tiers so background/subagent calls don't target a nonexistent Anthropic | ||
| // model id. | ||
| ANTHROPIC_DEFAULT_OPUS_MODEL: ctx.model, | ||
| ANTHROPIC_DEFAULT_SONNET_MODEL: ctx.model, | ||
| ANTHROPIC_DEFAULT_HAIKU_MODEL: ctx.model, | ||
| ANTHROPIC_DEFAULT_FABLE_MODEL: ctx.model, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If the caller already has Useful? React with 👍 / 👎.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in Confirmed against code.claude.com/docs/en/model-config — Change ( |
||
| // CLAUDE_CODE_SUBAGENT_MODEL overrides subagent/agent-team model resolution ahead of the | ||
| // tiers above (per code.claude.com/docs/en/model-config), so pin it too -- otherwise a value | ||
| // inherited from the caller's shell would send subagents to a cloud/nonexistent model, since | ||
| // spawnToolAndWait passes the parent env through unchanged for keys we don't set. | ||
| CLAUDE_CODE_SUBAGENT_MODEL: ctx.model, | ||
| }; | ||
| const notes: string[] = []; | ||
| if (ctx.contextLength !== undefined) { | ||
| env.CLAUDE_CODE_AUTO_COMPACT_WINDOW = String(ctx.contextLength); | ||
| } | ||
| if (CLAUDE_FIRST_PARTY_MODEL_ID_RE.test(ctx.model)) { | ||
| notes.push( | ||
| `Model id "${ctx.model}" starts with "claude-"; Claude Code will assume a first-party ` + | ||
| `200K window and ignore CLAUDE_CODE_AUTO_COMPACT_WINDOW. Load with ` + | ||
| `"lms load --identifier <other-name>" to avoid this.`, | ||
| ); | ||
| } | ||
| // args is empty on purpose: any passthrough "--model" the user typed after "claude" is | ||
| // forwarded untouched by index.ts, and Claude Code's own --model flag takes precedence over | ||
| // the env vars above, so nothing here needs to inject or dedupe a --model arg. | ||
| return { command: COMMAND, args: [], env, notes }; | ||
| }, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import { type ToolAdapter } from "../types.js"; | ||
|
|
||
| const COMMAND = "codex"; | ||
| // A synthetic provider id, scoped to this invocation only (never written to any config file). | ||
| const PROVIDER_ID = "lmslaunch"; | ||
|
|
||
| /** | ||
| * Codex CLI. Ships the explicit custom-provider `-c` override form (portable across versions) | ||
| * rather than assuming a built-in "lmstudio"/"oss" provider exists in the installed release. | ||
| * Uses wire_api=responses: current Codex removed Chat Completions support (Feb 2026) and only | ||
| * speaks the Responses API, which LM Studio serves at /v1/responses. See the note for the legacy | ||
| * chat fallback (older Codex + older LM Studio). | ||
| */ | ||
| export const codex: ToolAdapter = { | ||
| name: "codex", | ||
| displayName: "Codex CLI", | ||
| command: COMMAND, | ||
| install: { npm: "@openai/codex" }, | ||
| supportsContextHint: true, | ||
| async prepare(ctx) { | ||
| const args = [ | ||
| "-c", | ||
| `model_providers.${PROVIDER_ID}.base_url=${ctx.openaiBaseUrl}`, | ||
| "-c", | ||
| `model_providers.${PROVIDER_ID}.wire_api=responses`, | ||
| "-c", | ||
| // Names the env var Codex reads the bearer token from. Without env_key the custom provider | ||
| // sends no Authorization header, so `--api-key` can't authenticate a secured LM Studio endpoint. | ||
| `model_providers.${PROVIDER_ID}.env_key=OPENAI_API_KEY`, | ||
| "-c", | ||
| `model_provider=${PROVIDER_ID}`, | ||
| "-c", | ||
| `model=${ctx.model}`, | ||
| "-c", | ||
| "sandbox_mode=workspace-write", | ||
| ]; | ||
| if (ctx.contextLength !== undefined) { | ||
| args.push("-c", `model_context_window=${ctx.contextLength}`); | ||
| } | ||
| const env: Record<string, string> = { | ||
| // Referenced by model_providers.<id>.env_key above; Codex forwards it as the bearer token. | ||
| OPENAI_API_KEY: ctx.apiKey, | ||
| }; | ||
| const notes = [ | ||
| `Temporary Codex provider ("${PROVIDER_ID}") using wire_api=responses (current Codex dropped ` + | ||
| `Chat Completions; LM Studio serves /v1/responses). On a pre-2026 Codex without Responses ` + | ||
| `support, override after "--": -c model_providers.${PROVIDER_ID}.wire_api=chat`, | ||
| ]; | ||
| return { command: COMMAND, args, env, notes }; | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import { type ToolAdapter } from "../types.js"; | ||
|
|
||
| const COMMAND = "copilot"; | ||
|
|
||
| /** | ||
| * GitHub Copilot CLI (the standalone `@github/copilot` package, NOT `gh copilot`). Env-only. | ||
| * Verified names against https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/use-byok-models | ||
| */ | ||
| export const copilot: ToolAdapter = { | ||
| name: "copilot", | ||
| displayName: "GitHub Copilot CLI", | ||
| command: COMMAND, | ||
| install: { npm: "@github/copilot" }, | ||
| // No verified per-tool context knob: context lives entirely in the model load (layer 1). | ||
| supportsContextHint: false, | ||
| async prepare(ctx) { | ||
| const env: Record<string, string> = { | ||
| COPILOT_PROVIDER_BASE_URL: ctx.openaiBaseUrl, | ||
| COPILOT_MODEL: ctx.model, | ||
| COPILOT_PROVIDER_API_KEY: ctx.apiKey, | ||
| COPILOT_PROVIDER_TYPE: "openai", | ||
| COPILOT_OFFLINE: "true", // do not contact GitHub servers | ||
| }; | ||
| return { command: COMMAND, args: [], env }; | ||
| }, | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Registering
launchas a pass-through command here exposes it to the pre-Commander version check below (commandArguments.includes("-v") || commandArguments.includes("--version")). For any forwarded tool version flag, such aslms launch codex -- --versionorlms launch aider -- -v, the process exits beforelaunchcan parse the--separator, so the advertised verbatim forwarding path cannot invoke the tool.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in
d37632c.Right — the pre-Commander shortcut did a blunt
commandArguments.includes("-v"|"--version")over all args, solms launch codex -- --version(orlms launch aider -- -v) matched and printed the lms version + exited beforelaunchever parsed--, defeating the verbatim forwarding.Change (
src/index.ts): the shortcut now only fires for a-v/--versionthat appears before the first subcommand token (commandArguments.slice(0, firstSubcommandIndex)).lms -v/lms --versionstill work; after a subcommand the flag belongs to it, andlaunchforwards everything past--to the wrapped tool. As a bonus this also stopslms <anysub> --versionfrom masquerading as the lms version in general.