Repository navigation
fix(server): Claude text generation honors --settings launch args #25059
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?
Changes from all commits
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 |
|---|---|---|
| @@ -1,11 +1,13 @@ | ||
| import * as Effect from "effect/Effect"; | ||
| import * as FileSystem from "effect/FileSystem"; | ||
| import * as Option from "effect/Option"; | ||
| import * as Path from "effect/Path"; | ||
| import * as Schema from "effect/Schema"; | ||
| import * as Stream from "effect/Stream"; | ||
| import { ChildProcess, ChildProcessSpawner } from "effect/process"; | ||
|
|
||
| import { type ClaudeSettings, TextGenerationError } from "@t3tools/contracts"; | ||
| import { parseCliArgs } from "@t3tools/shared/cliArgs"; | ||
| import { resolveSpawnCommand } from "@t3tools/shared/shell"; | ||
|
|
||
| import * as TextGenerationOperations from "@t3tools/provider-core/server/textGenerationOperations"; | ||
|
|
@@ -65,6 +67,9 @@ const decodeClaudeOutput = Schema.decodeEffect( | |
|
|
||
| const ClaudeErrorResult = Schema.Struct({ is_error: Schema.Literal(true), result: Schema.String }); | ||
| const isClaudeErrorResult = Schema.is(ClaudeErrorResult); | ||
| const decodeLaunchSettings = Schema.decodeEffect( | ||
| Schema.fromJsonString(Schema.Record(Schema.String, Schema.Unknown)), | ||
| ); | ||
| const decodeJsonOption = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Unknown)); | ||
|
|
||
| // Claude reports JSON errors on stdout; stderr may contain only wrapper notices. | ||
|
|
@@ -84,6 +89,7 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu | |
| ) { | ||
| const commandSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; | ||
| const fileSystem = yield* FileSystem.FileSystem; | ||
| const path = yield* Path.Path; | ||
| const claudeEnvironment = yield* makeClaudeEnvironment(claudeSettings, environment); | ||
| const scopedModelCatalog = modelCatalog.pipe( | ||
| Effect.map((catalog) => scopeClaudeModelCatalog(catalog, claudeSettings.customModels)), | ||
|
|
@@ -120,6 +126,34 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu | |
| ), | ||
| ); | ||
|
|
||
| // Claude passes one --settings flag, so ours has to carry the launch-arg settings too. | ||
| // Like the CLI, a value starting with "{" is JSON and anything else is a file path. | ||
| const readLaunchSettings = Effect.fn("readLaunchSettings")(function* ( | ||
| operation: TextGenerationOperations.Operation, | ||
| cwd: string, | ||
| ) { | ||
| const raw = parseCliArgs(claudeSettings.launchArgs).flags.settings; | ||
| if (raw === undefined) return {}; | ||
| const value = raw?.trim() ?? ""; | ||
| const fail = (detail: string) => (cause: unknown) => | ||
| new TextGenerationError({ operation, detail, cause }); | ||
| if (value.length === 0) { | ||
| return yield* new TextGenerationError({ | ||
| operation, | ||
| detail: "The --settings launch argument needs a JSON object or file path.", | ||
| }); | ||
| } | ||
| const filePath = path.resolve(cwd, value); | ||
| const json = value.startsWith("{") | ||
| ? value | ||
| : yield* fileSystem | ||
| .readFileString(filePath) | ||
| .pipe(Effect.mapError(fail(`Failed to read Claude settings file ${filePath}.`))); | ||
| return yield* decodeLaunchSettings(json).pipe( | ||
| Effect.mapError(fail("The --settings launch argument is not a JSON object.")), | ||
| ); | ||
| }); | ||
|
|
||
| const runClaudeJson = Effect.fn("runClaudeJson")(function* <S extends Schema.Top>({ | ||
| operation, | ||
| cwd, | ||
|
|
@@ -170,6 +204,7 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu | |
| const fastMode = | ||
| fastModeDescriptor?.type === "boolean" ? fastModeDescriptor.currentValue : undefined; | ||
| const settings = { | ||
| ...(yield* readLaunchSettings(operation, cwd)), | ||
|
Contributor
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. 🔎 Supported by static analysis
Preserve an explicit
🐛 Suggested fix
|
||
| disableAllHooks: true, | ||
| ...(typeof thinking === "boolean" ? { alwaysThinkingEnabled: thinking } : {}), | ||
| ...(fastMode ? { fastMode: true } : {}), | ||
|
Contributor
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. 🟠 High Selecting 🚀 Reply "fix it for me" or copy this AI Prompt for your agent: |
||
|
|
||
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.
Keep the settings path out of the error detail.
If the file read fails,
filePathcopies an unbounded launch-argument value intoTextGenerationError.detailand its user-facing message. The path can contain sensitive text. Use a fixed detail and retain the read failure incause. As per coding guidelines, “Attributes and log annotations stay bounded: no raw payloads, command arguments or output, signed URLs, credentials, query strings, or arbitrary defect text.” As per path instructions, service errors use “structured, bounded attributes.”Sources: Coding guidelines, Path instructions