@johnowennixon/diffdash 1.9.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -118,7 +118,7 @@ diffdash --debug-llm-prompts
118
118
  All command-line arguments are optional.
119
119
 
120
120
  | Argument | Description |
121
- |--------|-------------|
121
+ | -------- | ----------- |
122
122
  | `--help` | show a help message and exit |
123
123
  | `--version` | show program version information and exit |
124
124
  | `--auto-add` | automatically stage all changes without confirmation |
@@ -126,17 +126,18 @@ All command-line arguments are optional.
126
126
  | `--auto-push` | automatically push changes after commit without confirmation |
127
127
  | `--disable-add` | disable adding unstaged changes - exit if no changes staged |
128
128
  | `--disable-status` | disable listing the staged files before generating a message |
129
- | `--disable-preview` | disable previewing the generated message|
129
+ | `--disable-preview` | disable previewing the generated message |
130
130
  | `--disable-commit` | disable committing changes - exit after generating the message |
131
131
  | `--disable-push` | disable pushing changes - exit after making the commit |
132
132
  | `--add-prefix PREFIX` | add a prefix to the commit message summary line |
133
133
  | `--add-suffix SUFFIX` | add a suffix to the commit message summary line |
134
- | `--no-verify` | bypass git hooks when committing or pushing to Git |
135
- | `--force` | apply force when pushing to Git |
136
134
  | `--llm-list` | display a list of available Large Language Models and exit |
137
135
  | `--llm-compare` | compare the generated messages from all models - but do not commit |
138
136
  | `--llm-model MODEL` | choose the LLM model by name (the default is normally best) |
139
137
  | `--llm-excludes MODELS` | models to exclude from comparison (comma separated) |
138
+ | `--no-secret-check` | bypass checking for secrets in diffs |
139
+ | `--no-verify` | bypass git hooks when committing or pushing to Git |
140
+ | `--force` | apply force when pushing to Git |
140
141
  | `--just-output` | just output the commit message for use in scripts |
141
142
  | `--silent` | suppress all normal output - errors and aborts still display |
142
143
  | `--debug-llm-prompts` | show prompts sent to the LLM |
@@ -153,6 +154,8 @@ Files containing secrets should not be in Git. But if they are, you can add an e
153
154
  .env -diff
154
155
  ```
155
156
 
157
+ There is a rudimentary check for secrets in diffs before submitting to the LLM. If any are found, there is an interactive option to ignore. If you want to bypass this check, you can use the `--no-secret-check` flag.
158
+
156
159
  ## Development
157
160
 
158
161
  To install on your laptop:
package/dist/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@johnowennixon/diffdash",
3
- "version": "1.9.0",
3
+ "version": "1.11.0",
4
4
  "description": "A command-line tool to generate Git commit messages using AI",
5
5
  "license": "0BSD",
6
6
  "author": "John Owen Nixon",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/johnowennixon/diffdash.git"
9
+ "url": "git+https://github.com/johnowennixon/diffdash.git"
10
10
  },
11
11
  "engines": {
12
12
  "node": ">=20"
@@ -42,33 +42,34 @@
42
42
  "test": "run-s -ls lint build"
43
43
  },
44
44
  "dependencies": {
45
- "@ai-sdk/anthropic": "2.0.9",
46
- "@ai-sdk/deepseek": "1.0.13",
47
- "@ai-sdk/google": "2.0.11",
48
- "@ai-sdk/openai": "2.0.23",
49
- "@inquirer/prompts": "7.8.4",
50
- "@openrouter/ai-sdk-provider": "1.1.2",
51
- "ai": "5.0.29",
52
- "ansis": "4.1.0",
45
+ "@ai-sdk/anthropic": "2.0.53",
46
+ "@ai-sdk/deepseek": "1.0.31",
47
+ "@ai-sdk/google": "2.0.44",
48
+ "@ai-sdk/openai": "2.0.77",
49
+ "@inquirer/prompts": "8.0.2",
50
+ "@openrouter/ai-sdk-provider": "1.2.3",
51
+ "ai": "5.0.102",
52
+ "ansis": "4.2.0",
53
53
  "argparse": "2.0.1",
54
54
  "cli-table3": "0.6.5",
55
55
  "json5": "2.2.3",
56
- "simple-git": "3.28.0",
57
- "zod": "4.1.5"
56
+ "magic-regexp": "0.10.0",
57
+ "simple-git": "3.30.0",
58
+ "zod": "4.1.13"
58
59
  },
59
60
  "devDependencies": {
60
- "@biomejs/biome": "2.2.2",
61
- "@candide/tsgolint": "1.3.0",
61
+ "@biomejs/biome": "2.3.8",
62
+ "@candide/tsgolint": "1.4.0",
62
63
  "@johnowennixon/add-shebangs": "1.1.0",
63
64
  "@johnowennixon/chmodx": "2.1.0",
64
65
  "@types/argparse": "2.0.17",
65
- "@types/node": "24.3.0",
66
- "@typescript/native-preview": "7.0.0-dev.20250902.1",
67
- "knip": "5.63.0",
68
- "markdownlint-cli2": "0.18.1",
66
+ "@types/node": "24.10.1",
67
+ "@typescript/native-preview": "7.0.0-dev.20251022.1",
68
+ "knip": "5.71.0",
69
+ "markdownlint-cli2": "0.19.1",
69
70
  "npm-run-all2": "8.0.4",
70
- "oxlint": "1.14.0",
71
- "rimraf": "6.0.1",
72
- "typescript": "5.9.2"
71
+ "oxlint": "1.31.0",
72
+ "rimraf": "6.1.2",
73
+ "typescript": "5.9.3"
73
74
  }
74
75
  }
@@ -8,3 +8,4 @@ export const DIGIT_6 = "6";
8
8
  export const DIGIT_7 = "7";
9
9
  export const DIGIT_8 = "8";
10
10
  export const DIGIT_9 = "9";
11
+ export const DIGITS = "0123456789";
@@ -1,4 +1,12 @@
1
+ import { QUOTE_DOUBLE, QUOTE_SINGLE } from "./lib_char_punctuation.js";
1
2
  export const LEFT_DOUBLE_QUOTATION_MARK = "“";
2
3
  export const LEFT_SINGLE_QUOTATION_MARK = "‘";
3
4
  export const RIGHT_DOUBLE_QUOTATION_MARK = "”";
4
5
  export const RIGHT_SINGLE_QUOTATION_MARK = "’";
6
+ export function char_smart_remove(text) {
7
+ return text
8
+ .replaceAll(LEFT_DOUBLE_QUOTATION_MARK, QUOTE_DOUBLE)
9
+ .replaceAll(LEFT_SINGLE_QUOTATION_MARK, QUOTE_SINGLE)
10
+ .replaceAll(RIGHT_DOUBLE_QUOTATION_MARK, QUOTE_DOUBLE)
11
+ .replaceAll(RIGHT_SINGLE_QUOTATION_MARK, QUOTE_SINGLE);
12
+ }
@@ -17,12 +17,6 @@ export function cli_integer_always(options = {}) {
17
17
  export function cli_boolean(options = {}) {
18
18
  return { kind: "boolean", options, value: false };
19
19
  }
20
- export function cli_command_sync(command_sync, options = {}) {
21
- return { kind: "boolean", options, value: false, command_sync };
22
- }
23
- export function cli_command_async(command_async, options = {}) {
24
- return { kind: "boolean", options, value: false, command_async };
25
- }
26
20
  export function cli_choice_optional(options = {}) {
27
21
  return { kind: "choice", options, value: undefined };
28
22
  }
@@ -126,50 +120,6 @@ function cli_recursive_parse({ schema, namespace, predicate, }) {
126
120
  }
127
121
  return result;
128
122
  }
129
- function cli_recursive_despatch_sync({ schema, namespace, parsed_args, }) {
130
- for (const key in schema) {
131
- if (!Object.hasOwn(schema, key)) {
132
- continue;
133
- }
134
- const cli = schema[key];
135
- if (!cli) {
136
- continue;
137
- }
138
- if (cli.kind === "meg") {
139
- const nested_schema = cli.value;
140
- cli_recursive_despatch_sync({ schema: nested_schema, namespace, parsed_args });
141
- }
142
- else if (cli.kind === "boolean") {
143
- if (namespace[key]) {
144
- if (cli.command_sync) {
145
- cli.command_sync(parsed_args);
146
- }
147
- }
148
- }
149
- }
150
- }
151
- async function cli_recursive_despatch_async({ schema, namespace, parsed_args, }) {
152
- for (const key in schema) {
153
- if (!Object.hasOwn(schema, key)) {
154
- continue;
155
- }
156
- const cli = schema[key];
157
- if (!cli) {
158
- continue;
159
- }
160
- if (cli.kind === "meg") {
161
- const nested_schema = cli.value;
162
- await cli_recursive_despatch_async({ schema: nested_schema, namespace, parsed_args });
163
- }
164
- else if (cli.kind === "boolean") {
165
- if (namespace[key]) {
166
- if (cli.command_async) {
167
- await cli.command_async(parsed_args);
168
- }
169
- }
170
- }
171
- }
172
- }
173
123
  export function cli_make_parser({ cli_schema, description, }) {
174
124
  const argument_parser_options = { description, allow_abbrev: false };
175
125
  const parser = new ArgumentParser(argument_parser_options);
@@ -177,11 +127,5 @@ export function cli_make_parser({ cli_schema, description, }) {
177
127
  const namespace = parser.parse_args();
178
128
  debug_inspect_when(debug_channels.cli, namespace, "namespace");
179
129
  const parsed_args = cli_recursive_parse({ schema: cli_schema, namespace });
180
- function despatch_sync() {
181
- cli_recursive_despatch_sync({ schema: cli_schema, namespace, parsed_args });
182
- }
183
- async function despatch_async() {
184
- await cli_recursive_despatch_async({ schema: cli_schema, namespace, parsed_args });
185
- }
186
- return { parsed_args, despatch_sync, despatch_async };
130
+ return { parsed_args };
187
131
  }
@@ -10,8 +10,6 @@ const diffdash_cli_schema = {
10
10
  disable_preview: cli_boolean({ help: "disable previewing the generated message" }),
11
11
  disable_commit: cli_boolean({ help: "disable committing changes - exit after generating the message" }),
12
12
  disable_push: cli_boolean({ help: "disable pushing changes - exit after making the commit" }),
13
- no_verify: cli_boolean({ help: "bypass git hooks when committing or pushing to Git" }),
14
- force: cli_boolean({ help: "apply force when pushing to Git" }),
15
13
  add_prefix: cli_string({ help: "add a prefix to the commit message summary line", metavar: "PREFIX" }),
16
14
  add_suffix: cli_string({ help: "add a suffix to the commit message summary line", metavar: "SUFFIX" }),
17
15
  llm_list: cli_boolean({ help: "display a list of available Large Language Models and exit" }),
@@ -22,6 +20,9 @@ const diffdash_cli_schema = {
22
20
  default: diffdash_llm_model_default,
23
21
  }),
24
22
  llm_excludes: cli_string({ help: "models to exclude from comparison (comma separated)", metavar: "MODELS" }),
23
+ no_secret_check: cli_boolean({ help: "bypass checking for secrets in diffs" }),
24
+ no_verify: cli_boolean({ help: "bypass git hooks when committing or pushing to Git" }),
25
+ force: cli_boolean({ help: "apply force when pushing to Git" }),
25
26
  just_output: cli_boolean({ help: "just output the commit message for use in scripts" }),
26
27
  silent: cli_boolean({ help: "suppress all normal output - errors and aborts still display" }),
27
28
  debug_llm_prompts: cli_boolean({ help: "debug prompts sent to the LLM" }),
@@ -33,7 +33,7 @@ function diffdash_config_file_read(config) {
33
33
  }
34
34
  }
35
35
  export function diffdash_config_get() {
36
- const { version, auto_add, auto_commit, auto_push, disable_add, disable_commit, disable_preview, disable_status, disable_push, no_verify, force, add_prefix, add_suffix, llm_list, llm_compare, llm_model, llm_excludes, just_output, silent, debug_llm_prompts, debug_llm_inputs, debug_llm_outputs, } = diffdash_cli_parsed_args;
36
+ const { version, auto_add, auto_commit, auto_push, disable_add, disable_commit, disable_preview, disable_status, disable_push, add_prefix, add_suffix, llm_list, llm_compare, llm_model, llm_excludes, no_secret_check, no_verify, force, just_output, silent, debug_llm_prompts, debug_llm_inputs, debug_llm_outputs, } = diffdash_cli_parsed_args;
37
37
  if (version) {
38
38
  tell_plain(`${PACKAGE_NAME} v${PACKAGE_VERSION}`);
39
39
  process.exit(0);
@@ -58,6 +58,7 @@ export function diffdash_config_get() {
58
58
  disable_push,
59
59
  add_prefix,
60
60
  add_suffix,
61
+ no_secret_check,
61
62
  no_verify,
62
63
  force,
63
64
  llm_compare,
@@ -13,7 +13,7 @@ const model_name_options = [
13
13
  "gpt-5-nano",
14
14
  "gpt-5-nano-minimal",
15
15
  "grok-code-fast-1",
16
- "llama-4-maverick@cerebras",
16
+ "llama-4-maverick@groq",
17
17
  ];
18
18
  export const diffdash_llm_model_details = llm_model_get_details({ llm_model_names: model_name_options });
19
19
  export const diffdash_llm_model_choices = llm_model_get_choices({ llm_model_details: diffdash_llm_model_details });
@@ -2,13 +2,14 @@ import { abort_with_error, abort_with_warning } from "./lib_abort.js";
2
2
  import { ansi_blue } from "./lib_ansi.js";
3
3
  import { debug_channels, debug_inspect } from "./lib_debug.js";
4
4
  import { diffdash_add_footer, diffdash_add_prefix_or_suffix } from "./lib_diffdash_add.js";
5
- import { error_get_text } from "./lib_error.js";
5
+ import { error_get_message, error_get_text } from "./lib_error.js";
6
6
  import { git_message_display } from "./lib_git_message_display.js";
7
7
  import { git_message_generate_result } from "./lib_git_message_generate.js";
8
8
  import { git_message_validate_check, git_message_validate_get_result } from "./lib_git_message_validate.js";
9
9
  import { git_simple_open_check_not_bare, git_simple_open_git_repo } from "./lib_git_simple_open.js";
10
10
  import { git_simple_staging_create_commit, git_simple_staging_get_staged_diff, git_simple_staging_get_staged_diffstat, git_simple_staging_has_staged_changes, git_simple_staging_has_unstaged_changes, git_simple_staging_push_to_remote, git_simple_staging_stage_all_changes, } from "./lib_git_simple_staging.js";
11
11
  import { llm_results_summary } from "./lib_llm_results.js";
12
+ import { secret_check } from "./lib_secret_check.js";
12
13
  import { stdio_write_stdout, stdio_write_stdout_linefeed } from "./lib_stdio_write.js";
13
14
  import { tell_action, tell_info, tell_plain, tell_success, tell_warning } from "./lib_tell.js";
14
15
  import { tui_confirm } from "./lib_tui_confirm.js";
@@ -92,13 +93,21 @@ async function phase_status({ config, git }) {
92
93
  }
93
94
  async function phase_compare({ config, git }) {
94
95
  const { silent } = config;
95
- if (!silent) {
96
- tell_action("Generating Git commit messages using all models in parallel");
97
- }
98
- const { all_llm_configs, add_prefix, add_suffix, extra_prompts } = config;
96
+ const { all_llm_configs, add_prefix, add_suffix, no_secret_check, extra_prompts } = config;
99
97
  const diffstat = await git_simple_staging_get_staged_diffstat(git);
100
98
  const diff = await git_simple_staging_get_staged_diff(git);
99
+ if (!no_secret_check) {
100
+ try {
101
+ await secret_check({ text: diff, interactive: true });
102
+ }
103
+ catch (error) {
104
+ abort_with_error(`Aborting: ${error_get_message(error)}`);
105
+ }
106
+ }
101
107
  const inputs = { diffstat, diff, extra_prompts };
108
+ if (!silent) {
109
+ tell_action("Generating Git commit messages using all models in parallel");
110
+ }
102
111
  const result_promises = all_llm_configs.map((llm_config) => git_message_generate_result({ llm_config, inputs }));
103
112
  const all_results = await Promise.all(result_promises);
104
113
  for (const result of all_results) {
@@ -120,14 +129,22 @@ async function phase_compare({ config, git }) {
120
129
  llm_results_summary(all_results);
121
130
  }
122
131
  async function phase_generate({ config, git }) {
123
- const { disable_preview, add_prefix, add_suffix, llm_config, just_output, silent, extra_prompts } = config;
132
+ const { disable_preview, add_prefix, add_suffix, llm_config, no_secret_check, just_output, silent, extra_prompts } = config;
124
133
  const { llm_model_name } = llm_config;
125
- if (!silent && !just_output) {
126
- tell_action(`Generating the Git commit message using ${llm_model_name}`);
127
- }
128
134
  const diffstat = await git_simple_staging_get_staged_diffstat(git);
129
135
  const diff = await git_simple_staging_get_staged_diff(git);
136
+ if (!no_secret_check) {
137
+ try {
138
+ await secret_check({ text: diff, interactive: true });
139
+ }
140
+ catch (error) {
141
+ abort_with_error(`Aborting: ${error_get_message(error)}`);
142
+ }
143
+ }
130
144
  const inputs = { diffstat, diff, extra_prompts };
145
+ if (!silent && !just_output) {
146
+ tell_action(`Generating the Git commit message using ${llm_model_name}`);
147
+ }
131
148
  const result = await git_message_generate_result({ llm_config, inputs });
132
149
  const { error_text } = result;
133
150
  if (error_text !== null) {
@@ -4,22 +4,23 @@ import { git_message_prompt_get_system, git_message_prompt_get_user } from "./li
4
4
  import { git_message_schema, git_message_schema_format } from "./lib_git_message_schema.js";
5
5
  import { llm_chat_generate_object, llm_chat_generate_text } from "./lib_llm_chat.js";
6
6
  import { llm_tokens_debug_usage, llm_tokens_estimate_length_from_tokens, llm_tokens_estimate_tokens_from_length, } from "./lib_llm_tokens.js";
7
- async function git_message_generate_unstructured({ llm_config, system_prompt, user_prompt, }) {
8
- const outputs = await llm_chat_generate_text({ llm_config, system_prompt, user_prompt });
7
+ async function git_message_generate_unstructured({ llm_config, system_prompt, user_prompt, max_output_tokens, }) {
8
+ const outputs = await llm_chat_generate_text({ llm_config, system_prompt, user_prompt, max_output_tokens });
9
9
  return outputs;
10
10
  }
11
- async function git_message_generate_structured({ llm_config, system_prompt, user_prompt, }) {
11
+ async function git_message_generate_structured({ llm_config, system_prompt, user_prompt, max_output_tokens, }) {
12
12
  const schema = git_message_schema;
13
13
  const { generated_object, total_usage, provider_metadata } = await llm_chat_generate_object({
14
14
  llm_config,
15
15
  system_prompt,
16
16
  user_prompt,
17
+ max_output_tokens,
17
18
  schema,
18
19
  });
19
20
  const generated_text = git_message_schema_format(generated_object);
20
21
  return { generated_text, reasoning_text: undefined, total_usage, provider_metadata };
21
22
  }
22
- export async function git_message_generate_string({ llm_config, inputs, }) {
23
+ async function git_message_generate_outputs({ llm_config, inputs, }) {
23
24
  const { effective_context_window } = llm_config;
24
25
  const { has_structured_json } = llm_config.llm_model_detail;
25
26
  const system_prompt = git_message_prompt_get_system({ has_structured_json, inputs });
@@ -30,10 +31,11 @@ export async function git_message_generate_string({ llm_config, inputs, }) {
30
31
  inputs,
31
32
  max_length: user_length,
32
33
  });
34
+ const max_output_tokens = 8192; // This is the maximum for some models
33
35
  llm_tokens_debug_usage({ name: "Inputs", llm_config, text: system_prompt + user_prompt });
34
36
  const outputs = has_structured_json
35
- ? await git_message_generate_structured({ llm_config, system_prompt, user_prompt })
36
- : await git_message_generate_unstructured({ llm_config, system_prompt, user_prompt });
37
+ ? await git_message_generate_structured({ llm_config, system_prompt, user_prompt, max_output_tokens })
38
+ : await git_message_generate_unstructured({ llm_config, system_prompt, user_prompt, max_output_tokens });
37
39
  llm_tokens_debug_usage({ name: "Outputs", llm_config, text: outputs.generated_text });
38
40
  return outputs;
39
41
  }
@@ -41,7 +43,7 @@ export async function git_message_generate_result({ llm_config, inputs, }) {
41
43
  const duration = new Duration();
42
44
  duration.start();
43
45
  try {
44
- const outputs = await git_message_generate_string({ llm_config, inputs });
46
+ const outputs = await git_message_generate_outputs({ llm_config, inputs });
45
47
  duration.stop();
46
48
  const seconds = duration.seconds_rounded();
47
49
  return { llm_config, seconds, error_text: null, outputs };
@@ -1,5 +1,6 @@
1
1
  import { LF } from "./lib_char_control.js";
2
2
  import { EMPTY } from "./lib_char_empty.js";
3
+ import { tell_warning } from "./lib_tell.js";
3
4
  const LF_LF = LF + LF;
4
5
  const portion_role = `
5
6
  Your role is to generate a Git commit message in conversational English.
@@ -65,6 +66,9 @@ export function git_message_prompt_get_system({ has_structured_json, inputs, })
65
66
  export function git_message_prompt_get_user({ has_structured_json, inputs, max_length, }) {
66
67
  const { diffstat, diff } = inputs;
67
68
  const truncate = diffstat.length + diff.length > max_length;
69
+ if (truncate) {
70
+ tell_warning("The Diff is too long to fit in the user prompt - it is being truncated");
71
+ }
68
72
  const diff_truncated = truncate ? diff.slice(0, max_length - diffstat.length) + LF : diff;
69
73
  let user_prompt = EMPTY;
70
74
  user_prompt += "<diffstat>" + LF + diffstat + "</diffstat>" + LF_LF;
@@ -10,7 +10,7 @@ import { tui_block_string } from "./lib_tui_block.js";
10
10
  function llm_chat_get_parameters() {
11
11
  return {
12
12
  max_output_tokens: parse_int_or_undefined(env_get_empty("lib_llm_chat_max_output_tokens")),
13
- timeout: parse_int(env_get_substitute("lib_llm_chat_timeout", "60")),
13
+ timeout: parse_int(env_get_substitute("lib_llm_chat_timeout", "90")),
14
14
  };
15
15
  }
16
16
  function llm_chat_debug_prompts({ llm_model_name, system_prompt, user_prompt, }) {
@@ -22,7 +22,7 @@ function llm_chat_debug_prompts({ llm_model_name, system_prompt, user_prompt, })
22
22
  tui_block_string({ teller, title: `LLM user prompt (for ${llm_model_name}):`, content: user_prompt });
23
23
  }
24
24
  }
25
- export async function llm_chat_generate_text({ llm_config, system_prompt, user_prompt, tools, max_steps, min_steps, }) {
25
+ export async function llm_chat_generate_text({ llm_config, system_prompt, user_prompt, max_output_tokens, tools, max_steps, min_steps, }) {
26
26
  const { llm_model_name, llm_model_detail, llm_model_code, llm_api_code, llm_api_key } = llm_config;
27
27
  llm_chat_debug_prompts({ system_prompt, user_prompt, llm_model_name });
28
28
  const ai_sdk_language_model = llm_api_get_ai_sdk_language_model({
@@ -30,9 +30,14 @@ export async function llm_chat_generate_text({ llm_config, system_prompt, user_p
30
30
  llm_api_code,
31
31
  llm_api_key,
32
32
  });
33
- const { recommended_temperature, provider_options } = llm_model_detail;
33
+ const { recommended_temperature, provider_options, max_output_tokens: max_output_tokens_model } = llm_model_detail;
34
34
  const temperature = recommended_temperature;
35
- const { max_output_tokens, timeout } = llm_chat_get_parameters();
35
+ const { timeout, max_output_tokens: max_output_tokens_env } = llm_chat_get_parameters();
36
+ if (max_output_tokens_env !== undefined) {
37
+ max_output_tokens = max_output_tokens_env;
38
+ }
39
+ max_output_tokens =
40
+ max_output_tokens === undefined ? max_output_tokens_model : Math.min(max_output_tokens, max_output_tokens_model);
36
41
  const llm_inputs = {
37
42
  model: ai_sdk_language_model,
38
43
  system: system_prompt,
@@ -57,11 +62,11 @@ export async function llm_chat_generate_text({ llm_config, system_prompt, user_p
57
62
  const { text: generated_text, reasoningText: reasoning_text, totalUsage: total_usage, providerMetadata: provider_metadata, } = llm_outputs;
58
63
  return { generated_text, reasoning_text, total_usage, provider_metadata };
59
64
  }
60
- export async function llm_chat_generate_text_result({ llm_config, system_prompt, user_prompt, }) {
65
+ export async function llm_chat_generate_text_result({ llm_config, system_prompt, user_prompt, max_output_tokens, }) {
61
66
  const duration = new Duration();
62
67
  duration.start();
63
68
  try {
64
- const outputs = await llm_chat_generate_text({ llm_config, system_prompt, user_prompt });
69
+ const outputs = await llm_chat_generate_text({ llm_config, system_prompt, user_prompt, max_output_tokens });
65
70
  duration.stop();
66
71
  const seconds = duration.seconds_rounded();
67
72
  return { llm_config, seconds, error_text: null, outputs };
@@ -73,7 +78,7 @@ export async function llm_chat_generate_text_result({ llm_config, system_prompt,
73
78
  return { llm_config, seconds, error_text, outputs: null };
74
79
  }
75
80
  }
76
- export async function llm_chat_generate_object({ llm_config, user_prompt, system_prompt, schema, }) {
81
+ export async function llm_chat_generate_object({ llm_config, user_prompt, system_prompt, max_output_tokens, schema, }) {
77
82
  const { llm_model_name, llm_model_detail, llm_model_code, llm_api_code, llm_api_key } = llm_config;
78
83
  llm_chat_debug_prompts({ system_prompt, user_prompt, llm_model_name });
79
84
  const ai_sdk_language_model = llm_api_get_ai_sdk_language_model({
@@ -83,14 +88,14 @@ export async function llm_chat_generate_object({ llm_config, user_prompt, system
83
88
  });
84
89
  const { recommended_temperature, provider_options } = llm_model_detail;
85
90
  const temperature = recommended_temperature;
86
- const { max_output_tokens, timeout } = llm_chat_get_parameters();
91
+ const { timeout, max_output_tokens: max_output_tokens_env } = llm_chat_get_parameters();
87
92
  const llm_inputs = {
88
93
  model: ai_sdk_language_model,
89
94
  system: system_prompt,
90
95
  prompt: user_prompt,
91
96
  output: "object",
92
97
  schema,
93
- maxOutputTokens: max_output_tokens,
98
+ maxOutputTokens: max_output_tokens_env ?? max_output_tokens,
94
99
  temperature,
95
100
  providerOptions: provider_options,
96
101
  abortSignal: AbortSignal.timeout(timeout * 1000),
@@ -18,6 +18,7 @@ export function llm_list_models({ llm_model_details }) {
18
18
  table.push(row);
19
19
  }
20
20
  stdio_write_stdout_linefeed(table.toString());
21
+ tell_info(`This is a total of ${llm_model_details.length} models.`);
21
22
  tell_info("Prices are per million tokens.");
22
23
  tell_warning("Prices are best effort and are liable to change - always double-check with your LLM API provider.");
23
24
  }
@@ -15,6 +15,15 @@ function provider_options_anthropic({ thinking }) {
15
15
  }
16
16
  : undefined;
17
17
  }
18
+ function provider_options_google({ thinking_level }) {
19
+ return {
20
+ google: {
21
+ thinkingConfig: {
22
+ thinkingLevel: thinking_level,
23
+ },
24
+ },
25
+ };
26
+ }
18
27
  function provider_options_openai({ reasoning_effort, }) {
19
28
  return {
20
29
  openai: {
@@ -37,6 +46,7 @@ export const LLM_MODEL_DETAILS = [
37
46
  llm_model_code: "claude-3-5-haiku-latest",
38
47
  llm_api_code: "anthropic",
39
48
  context_window: 200_000,
49
+ max_output_tokens: 8192,
40
50
  cents_input: 80,
41
51
  cents_output: 400,
42
52
  default_reasoning: false,
@@ -45,22 +55,37 @@ export const LLM_MODEL_DETAILS = [
45
55
  provider_options: provider_options_anthropic({ thinking: false }),
46
56
  },
47
57
  {
48
- llm_model_name: "claude-3.7-sonnet",
49
- llm_model_code: "claude-3-7-sonnet-latest",
58
+ llm_model_name: "claude-opus-4.5",
59
+ llm_model_code: "claude-opus-4-5",
50
60
  llm_api_code: "anthropic",
51
61
  context_window: 200_000,
52
- cents_input: 300,
53
- cents_output: 1500,
62
+ max_output_tokens: 64_000,
63
+ cents_input: 300, // for input tokens <= 200K
64
+ cents_output: 1500, // for input tokens <= 200K
54
65
  default_reasoning: false,
55
66
  has_structured_json: true,
56
67
  recommended_temperature: undefined,
57
68
  provider_options: provider_options_anthropic({ thinking: false }),
58
69
  },
70
+ {
71
+ llm_model_name: "claude-opus-4.5-thinking",
72
+ llm_model_code: "claude-opus-4-5",
73
+ llm_api_code: "anthropic",
74
+ context_window: 200_000,
75
+ max_output_tokens: 64_000 - 1024,
76
+ cents_input: 300, // for input tokens <= 200K
77
+ cents_output: 1500, // for input tokens <= 200K
78
+ default_reasoning: false,
79
+ has_structured_json: true,
80
+ recommended_temperature: undefined,
81
+ provider_options: provider_options_anthropic({ thinking: true }),
82
+ },
59
83
  {
60
84
  llm_model_name: "claude-sonnet-4",
61
85
  llm_model_code: "claude-sonnet-4-0",
62
86
  llm_api_code: "anthropic",
63
87
  context_window: 200_000,
88
+ max_output_tokens: 64_000,
64
89
  cents_input: 300,
65
90
  cents_output: 1500,
66
91
  default_reasoning: false,
@@ -73,6 +98,7 @@ export const LLM_MODEL_DETAILS = [
73
98
  llm_model_code: "claude-sonnet-4-0",
74
99
  llm_api_code: "anthropic",
75
100
  context_window: 200_000,
101
+ max_output_tokens: 62_976, // = 64000 - 1024 used for reasoning
76
102
  cents_input: 300,
77
103
  cents_output: 1500,
78
104
  default_reasoning: true,
@@ -81,22 +107,37 @@ export const LLM_MODEL_DETAILS = [
81
107
  provider_options: provider_options_anthropic({ thinking: true }),
82
108
  },
83
109
  {
84
- llm_model_name: "codestral-2508",
85
- llm_model_code: "mistralai/codestral-2508",
86
- llm_api_code: "openrouter",
87
- context_window: 256_000,
88
- cents_input: 30,
89
- cents_output: 90,
110
+ llm_model_name: "claude-sonnet-4.5",
111
+ llm_model_code: "claude-sonnet-4-5",
112
+ llm_api_code: "anthropic",
113
+ context_window: 200_000, // 1_000_000 available with context-1m beta header
114
+ max_output_tokens: 64_000,
115
+ cents_input: 300, // for input tokens <= 200K
116
+ cents_output: 1500, // for input tokens <= 200K
90
117
  default_reasoning: false,
91
118
  has_structured_json: true,
92
119
  recommended_temperature: undefined,
93
- provider_options: provider_options_openrouter({ only: "mistral" }),
120
+ provider_options: provider_options_anthropic({ thinking: false }),
121
+ },
122
+ {
123
+ llm_model_name: "claude-sonnet-4.5-thinking",
124
+ llm_model_code: "claude-sonnet-4-5",
125
+ llm_api_code: "anthropic",
126
+ context_window: 200_000, // 1_000_000 available with context-1m beta header
127
+ max_output_tokens: 62_976, // = 64000 - 1024 used for reasoning
128
+ cents_input: 300, // for input tokens <= 200K
129
+ cents_output: 1500, // for input tokens <= 200K
130
+ default_reasoning: false,
131
+ has_structured_json: true,
132
+ recommended_temperature: undefined,
133
+ provider_options: provider_options_anthropic({ thinking: true }),
94
134
  },
95
135
  {
96
136
  llm_model_name: "deepseek-chat",
97
137
  llm_model_code: "deepseek-chat",
98
138
  llm_api_code: "deepseek",
99
139
  context_window: 128_000,
140
+ max_output_tokens: 8192,
100
141
  cents_input: 56,
101
142
  cents_output: 168,
102
143
  default_reasoning: false,
@@ -109,6 +150,7 @@ export const LLM_MODEL_DETAILS = [
109
150
  llm_model_code: "deepseek-reasoner",
110
151
  llm_api_code: "deepseek",
111
152
  context_window: 128_000,
153
+ max_output_tokens: 65_536,
112
154
  cents_input: 56,
113
155
  cents_output: 168,
114
156
  default_reasoning: true,
@@ -121,6 +163,7 @@ export const LLM_MODEL_DETAILS = [
121
163
  llm_model_code: "mistralai/devstral-medium",
122
164
  llm_api_code: "openrouter",
123
165
  context_window: 128_000,
166
+ max_output_tokens: 128_000,
124
167
  cents_input: 40,
125
168
  cents_output: 200,
126
169
  default_reasoning: false,
@@ -133,6 +176,7 @@ export const LLM_MODEL_DETAILS = [
133
176
  llm_model_code: "mistralai/devstral-small",
134
177
  llm_api_code: "openrouter",
135
178
  context_window: 128_000,
179
+ max_output_tokens: 128_000,
136
180
  cents_input: 10,
137
181
  cents_output: 30,
138
182
  default_reasoning: false,
@@ -145,6 +189,7 @@ export const LLM_MODEL_DETAILS = [
145
189
  llm_model_code: "gemini-2.0-flash",
146
190
  llm_api_code: "google",
147
191
  context_window: 1_048_576,
192
+ max_output_tokens: 8192,
148
193
  cents_input: 10,
149
194
  cents_output: 40,
150
195
  default_reasoning: false,
@@ -157,6 +202,7 @@ export const LLM_MODEL_DETAILS = [
157
202
  llm_model_code: "gemini-2.5-flash",
158
203
  llm_api_code: "google",
159
204
  context_window: 1_048_576,
205
+ max_output_tokens: 65_536,
160
206
  cents_input: 30,
161
207
  cents_output: 250,
162
208
  default_reasoning: false,
@@ -169,54 +215,85 @@ export const LLM_MODEL_DETAILS = [
169
215
  llm_model_code: "gemini-2.5-pro",
170
216
  llm_api_code: "google",
171
217
  context_window: 1_048_576,
218
+ max_output_tokens: 65_536,
172
219
  cents_input: 125,
173
220
  cents_output: 1000,
174
- default_reasoning: false,
221
+ default_reasoning: true,
175
222
  has_structured_json: true,
176
223
  recommended_temperature: undefined,
177
224
  provider_options: undefined,
178
225
  },
179
226
  {
180
- llm_model_name: "glm-4-32b@z-ai",
181
- llm_model_code: "z-ai/glm-4-32b",
182
- llm_api_code: "openrouter",
183
- context_window: 128_000,
184
- cents_input: 10,
185
- cents_output: 10,
227
+ llm_model_name: "gemini-3-pro-preview-high",
228
+ llm_model_code: "gemini-3-pro-preview",
229
+ llm_api_code: "google",
230
+ context_window: 1_048_576,
231
+ max_output_tokens: 65_536,
232
+ cents_input: 200,
233
+ cents_output: 1200,
234
+ default_reasoning: true,
235
+ has_structured_json: true,
236
+ recommended_temperature: undefined,
237
+ provider_options: provider_options_google({ thinking_level: "high" }),
238
+ },
239
+ {
240
+ llm_model_name: "gemini-3-pro-preview-low",
241
+ llm_model_code: "gemini-3-pro-preview",
242
+ llm_api_code: "google",
243
+ context_window: 1_048_576,
244
+ max_output_tokens: 65_536,
245
+ cents_input: 200,
246
+ cents_output: 1200,
186
247
  default_reasoning: false,
187
- has_structured_json: false,
248
+ has_structured_json: true,
188
249
  recommended_temperature: undefined,
189
- provider_options: provider_options_openrouter({ only: "z-ai" }),
250
+ provider_options: provider_options_google({ thinking_level: "low" }),
190
251
  },
191
252
  {
192
253
  llm_model_name: "glm-4.5@z-ai",
193
254
  llm_model_code: "z-ai/glm-4.5",
194
255
  llm_api_code: "openrouter",
195
256
  context_window: 128_000,
257
+ max_output_tokens: 96_000,
196
258
  cents_input: 60,
197
259
  cents_output: 220,
198
260
  default_reasoning: true,
199
261
  has_structured_json: false,
200
262
  recommended_temperature: undefined,
201
- provider_options: provider_options_openrouter({ only: "z-ai/fp8" }),
263
+ provider_options: provider_options_openrouter({ only: "z-ai" }),
202
264
  },
203
265
  {
204
266
  llm_model_name: "glm-4.5-air@z-ai",
205
267
  llm_model_code: "z-ai/glm-4.5-air",
206
268
  llm_api_code: "openrouter",
207
269
  context_window: 128_000,
270
+ max_output_tokens: 96_000,
208
271
  cents_input: 20,
209
272
  cents_output: 110,
210
273
  default_reasoning: true,
211
274
  has_structured_json: false,
212
275
  recommended_temperature: undefined,
213
- provider_options: provider_options_openrouter({ only: "z-ai/fp8" }),
276
+ provider_options: provider_options_openrouter({ only: "z-ai" }),
277
+ },
278
+ {
279
+ llm_model_name: "glm-4.6@z-ai",
280
+ llm_model_code: "z-ai/glm-4.6",
281
+ llm_api_code: "openrouter",
282
+ context_window: 128_000,
283
+ max_output_tokens: 96_000,
284
+ cents_input: 60,
285
+ cents_output: 220,
286
+ default_reasoning: true,
287
+ has_structured_json: false,
288
+ recommended_temperature: undefined,
289
+ provider_options: provider_options_openrouter({ only: "z-ai" }),
214
290
  },
215
291
  {
216
292
  llm_model_name: "gpt-4.1",
217
293
  llm_model_code: "gpt-4.1",
218
294
  llm_api_code: "openai",
219
295
  context_window: context_window_openai({ tier1: 200_000, unrestricted: 1_000_000 }),
296
+ max_output_tokens: 32_768,
220
297
  cents_input: 200,
221
298
  cents_output: 800,
222
299
  default_reasoning: false,
@@ -229,6 +306,7 @@ export const LLM_MODEL_DETAILS = [
229
306
  llm_model_code: "gpt-4.1-mini",
230
307
  llm_api_code: "openai",
231
308
  context_window: context_window_openai({ tier1: 400_000, unrestricted: 1_000_000 }),
309
+ max_output_tokens: 32_768,
232
310
  cents_input: 40,
233
311
  cents_output: 160,
234
312
  default_reasoning: false,
@@ -241,6 +319,7 @@ export const LLM_MODEL_DETAILS = [
241
319
  llm_model_code: "gpt-4.1-nano",
242
320
  llm_api_code: "openai",
243
321
  context_window: context_window_openai({ tier1: 400_000, unrestricted: 1_000_000 }),
322
+ max_output_tokens: 32_768,
244
323
  cents_input: 10,
245
324
  cents_output: 40,
246
325
  default_reasoning: false,
@@ -253,6 +332,7 @@ export const LLM_MODEL_DETAILS = [
253
332
  llm_model_code: "gpt-5",
254
333
  llm_api_code: "openai",
255
334
  context_window: context_window_openai({ tier1: 30_000, unrestricted: 272_000 }),
335
+ max_output_tokens: 128_000,
256
336
  cents_input: 125,
257
337
  cents_output: 1000,
258
338
  default_reasoning: true,
@@ -265,6 +345,7 @@ export const LLM_MODEL_DETAILS = [
265
345
  llm_model_code: "gpt-5",
266
346
  llm_api_code: "openai",
267
347
  context_window: context_window_openai({ tier1: 30_000, unrestricted: 272_000 }),
348
+ max_output_tokens: 128_000,
268
349
  cents_input: 125,
269
350
  cents_output: 1000,
270
351
  default_reasoning: false,
@@ -277,6 +358,7 @@ export const LLM_MODEL_DETAILS = [
277
358
  llm_model_code: "gpt-5-mini",
278
359
  llm_api_code: "openai",
279
360
  context_window: context_window_openai({ tier1: 200_000, unrestricted: 272_000 }),
361
+ max_output_tokens: 128_000,
280
362
  cents_input: 25,
281
363
  cents_output: 200,
282
364
  default_reasoning: true,
@@ -289,6 +371,7 @@ export const LLM_MODEL_DETAILS = [
289
371
  llm_model_code: "gpt-5-mini",
290
372
  llm_api_code: "openai",
291
373
  context_window: context_window_openai({ tier1: 200_000, unrestricted: 272_000 }),
374
+ max_output_tokens: 128_000,
292
375
  cents_input: 25,
293
376
  cents_output: 200,
294
377
  default_reasoning: true,
@@ -301,6 +384,7 @@ export const LLM_MODEL_DETAILS = [
301
384
  llm_model_code: "gpt-5-mini",
302
385
  llm_api_code: "openai",
303
386
  context_window: context_window_openai({ tier1: 200_000, unrestricted: 272_000 }),
387
+ max_output_tokens: 128_000,
304
388
  cents_input: 25,
305
389
  cents_output: 200,
306
390
  default_reasoning: true,
@@ -313,6 +397,7 @@ export const LLM_MODEL_DETAILS = [
313
397
  llm_model_code: "gpt-5-mini",
314
398
  llm_api_code: "openai",
315
399
  context_window: context_window_openai({ tier1: 200_000, unrestricted: 272_000 }),
400
+ max_output_tokens: 128_000,
316
401
  cents_input: 25,
317
402
  cents_output: 200,
318
403
  default_reasoning: true,
@@ -325,6 +410,7 @@ export const LLM_MODEL_DETAILS = [
325
410
  llm_model_code: "gpt-5-mini",
326
411
  llm_api_code: "openai",
327
412
  context_window: context_window_openai({ tier1: 200_000, unrestricted: 272_000 }),
413
+ max_output_tokens: 128_000,
328
414
  cents_input: 25,
329
415
  cents_output: 200,
330
416
  default_reasoning: false,
@@ -337,6 +423,7 @@ export const LLM_MODEL_DETAILS = [
337
423
  llm_model_code: "gpt-5-nano",
338
424
  llm_api_code: "openai",
339
425
  context_window: context_window_openai({ tier1: 200_000, unrestricted: 272_000 }),
426
+ max_output_tokens: 128_000,
340
427
  cents_input: 5,
341
428
  cents_output: 40,
342
429
  default_reasoning: true,
@@ -349,6 +436,7 @@ export const LLM_MODEL_DETAILS = [
349
436
  llm_model_code: "gpt-5-nano",
350
437
  llm_api_code: "openai",
351
438
  context_window: context_window_openai({ tier1: 200_000, unrestricted: 272_000 }),
439
+ max_output_tokens: 128_000,
352
440
  cents_input: 5,
353
441
  cents_output: 40,
354
442
  default_reasoning: false,
@@ -361,6 +449,7 @@ export const LLM_MODEL_DETAILS = [
361
449
  llm_model_code: "openai/gpt-oss-120b",
362
450
  llm_api_code: "openrouter",
363
451
  context_window: 131_072,
452
+ max_output_tokens: 32_768,
364
453
  cents_input: 25,
365
454
  cents_output: 69,
366
455
  default_reasoning: false,
@@ -373,6 +462,7 @@ export const LLM_MODEL_DETAILS = [
373
462
  llm_model_code: "openai/gpt-oss-120b",
374
463
  llm_api_code: "openrouter",
375
464
  context_window: 131_072,
465
+ max_output_tokens: 65_536,
376
466
  cents_input: 15,
377
467
  cents_output: 75,
378
468
  default_reasoning: false,
@@ -385,6 +475,7 @@ export const LLM_MODEL_DETAILS = [
385
475
  llm_model_code: "x-ai/grok-3",
386
476
  llm_api_code: "openrouter",
387
477
  context_window: 131_072,
478
+ max_output_tokens: 131_072,
388
479
  cents_input: 300,
389
480
  cents_output: 1500,
390
481
  default_reasoning: true,
@@ -397,6 +488,7 @@ export const LLM_MODEL_DETAILS = [
397
488
  llm_model_code: "x-ai/grok-3-mini",
398
489
  llm_api_code: "openrouter",
399
490
  context_window: 131_072,
491
+ max_output_tokens: 131_072,
400
492
  cents_input: 30,
401
493
  cents_output: 50,
402
494
  default_reasoning: true,
@@ -404,47 +496,51 @@ export const LLM_MODEL_DETAILS = [
404
496
  recommended_temperature: undefined,
405
497
  provider_options: undefined,
406
498
  },
407
- // {
408
- // llm_model_name: "grok-4",
409
- // llm_model_code: "x-ai/grok-4", // BYOK required
410
- // llm_api_code: "openrouter",
411
- // context_window: 256_000,
412
- // cents_input: 300,
413
- // cents_output: 1500,
414
- // default_reasoning: true,
415
- // has_structured_json: true,
416
- // recommended_temperature: undefined,
417
- // provider_options: undefined,
418
- // },
419
499
  {
420
- llm_model_name: "grok-code-fast-1",
421
- llm_model_code: "x-ai/grok-code-fast-1",
500
+ llm_model_name: "grok-4",
501
+ llm_model_code: "x-ai/grok-4",
422
502
  llm_api_code: "openrouter",
423
503
  context_window: 256_000,
424
- cents_input: 20,
425
- cents_output: 150,
504
+ max_output_tokens: 256_000,
505
+ cents_input: 300,
506
+ cents_output: 1500,
426
507
  default_reasoning: true,
427
508
  has_structured_json: true,
428
509
  recommended_temperature: undefined,
429
510
  provider_options: undefined,
430
511
  },
431
512
  {
432
- llm_model_name: "kimi-k2-0711@groq",
433
- llm_model_code: "moonshotai/kimi-k2",
513
+ llm_model_name: "grok-4-fast",
514
+ llm_model_code: "x-ai/grok-4-fast",
434
515
  llm_api_code: "openrouter",
435
- context_window: 131_072,
436
- cents_input: 100,
437
- cents_output: 300,
438
- default_reasoning: false,
439
- has_structured_json: false,
516
+ context_window: 2_000_000,
517
+ max_output_tokens: 30_000,
518
+ cents_input: 20, // for input tokens <= 128K
519
+ cents_output: 50, // for input tokens <= 128K
520
+ default_reasoning: true,
521
+ has_structured_json: true,
440
522
  recommended_temperature: undefined,
441
- provider_options: provider_options_openrouter({ only: "groq" }),
523
+ provider_options: undefined,
524
+ },
525
+ {
526
+ llm_model_name: "grok-code-fast-1",
527
+ llm_model_code: "x-ai/grok-code-fast-1",
528
+ llm_api_code: "openrouter",
529
+ context_window: 256_000,
530
+ max_output_tokens: 10_000,
531
+ cents_input: 20,
532
+ cents_output: 150,
533
+ default_reasoning: true,
534
+ has_structured_json: true,
535
+ recommended_temperature: undefined,
536
+ provider_options: undefined,
442
537
  },
443
538
  {
444
539
  llm_model_name: "kimi-k2-0711@moonshotai",
445
540
  llm_model_code: "moonshotai/kimi-k2",
446
541
  llm_api_code: "openrouter",
447
542
  context_window: 131_072,
543
+ max_output_tokens: 131_072,
448
544
  cents_input: 60,
449
545
  cents_output: 250,
450
546
  default_reasoning: false,
@@ -457,6 +553,7 @@ export const LLM_MODEL_DETAILS = [
457
553
  llm_model_code: "moonshotai/kimi-k2-0905",
458
554
  llm_api_code: "openrouter",
459
555
  context_window: 262_144,
556
+ max_output_tokens: 16_384,
460
557
  cents_input: 100,
461
558
  cents_output: 300,
462
559
  default_reasoning: false,
@@ -465,34 +562,50 @@ export const LLM_MODEL_DETAILS = [
465
562
  provider_options: provider_options_openrouter({ only: "groq" }),
466
563
  },
467
564
  {
468
- llm_model_name: "llama-4-maverick@cerebras",
565
+ llm_model_name: "llama-4-maverick@groq",
469
566
  llm_model_code: "meta-llama/llama-4-maverick",
470
567
  llm_api_code: "openrouter",
471
- context_window: 32_000,
568
+ context_window: 131_072,
569
+ max_output_tokens: 8192,
472
570
  cents_input: 20,
473
571
  cents_output: 60,
474
572
  default_reasoning: false,
475
573
  has_structured_json: true,
476
574
  recommended_temperature: undefined,
477
- provider_options: provider_options_openrouter({ only: "cerebras" }),
575
+ provider_options: provider_options_openrouter({ only: "groq" }),
478
576
  },
479
577
  {
480
- llm_model_name: "llama-4-scout@cerebras",
578
+ llm_model_name: "llama-4-scout@groq",
481
579
  llm_model_code: "meta-llama/llama-4-scout",
482
580
  llm_api_code: "openrouter",
483
- context_window: 32_000,
484
- cents_input: 65,
485
- cents_output: 85,
581
+ context_window: 131_072,
582
+ max_output_tokens: 8192,
583
+ cents_input: 11,
584
+ cents_output: 34,
486
585
  default_reasoning: false,
487
586
  has_structured_json: true,
488
587
  recommended_temperature: undefined,
489
- provider_options: provider_options_openrouter({ only: "cerebras" }),
588
+ provider_options: provider_options_openrouter({ only: "groq" }),
589
+ },
590
+ {
591
+ llm_model_name: "longcat-flash",
592
+ llm_model_code: "meituan/longcat-flash-chat",
593
+ llm_api_code: "openrouter",
594
+ context_window: 131_072,
595
+ max_output_tokens: 131_072,
596
+ cents_input: 15,
597
+ cents_output: 75,
598
+ default_reasoning: false,
599
+ has_structured_json: true,
600
+ recommended_temperature: undefined,
601
+ provider_options: undefined,
490
602
  },
491
603
  {
492
604
  llm_model_name: "mercury",
493
605
  llm_model_code: "inception/mercury",
494
606
  llm_api_code: "openrouter",
495
- context_window: 32_000,
607
+ context_window: 128_000,
608
+ max_output_tokens: 16_384,
496
609
  cents_input: 25,
497
610
  cents_output: 100,
498
611
  default_reasoning: false,
@@ -504,7 +617,8 @@ export const LLM_MODEL_DETAILS = [
504
617
  llm_model_name: "mercury-coder",
505
618
  llm_model_code: "inception/mercury-coder-small-beta",
506
619
  llm_api_code: "openrouter",
507
- context_window: 32_000,
620
+ context_window: 128_000,
621
+ max_output_tokens: 16_384,
508
622
  cents_input: 25,
509
623
  cents_output: 100,
510
624
  default_reasoning: false,
@@ -516,7 +630,8 @@ export const LLM_MODEL_DETAILS = [
516
630
  llm_model_name: "mistral-medium-3.1",
517
631
  llm_model_code: "mistralai/mistral-medium-3.1",
518
632
  llm_api_code: "openrouter",
519
- context_window: 262_144,
633
+ context_window: 131_072,
634
+ max_output_tokens: 131_072,
520
635
  cents_input: 40,
521
636
  cents_output: 200,
522
637
  default_reasoning: false,
@@ -528,7 +643,8 @@ export const LLM_MODEL_DETAILS = [
528
643
  llm_model_name: "qwen3-235b-a22b-2507-instruct@cerebras",
529
644
  llm_model_code: "qwen/qwen3-235b-a22b-2507",
530
645
  llm_api_code: "openrouter",
531
- context_window: 262_144,
646
+ context_window: 131_072,
647
+ max_output_tokens: 131_072,
532
648
  cents_input: 60,
533
649
  cents_output: 120,
534
650
  default_reasoning: false,
@@ -536,41 +652,31 @@ export const LLM_MODEL_DETAILS = [
536
652
  recommended_temperature: undefined,
537
653
  provider_options: provider_options_openrouter({ only: "cerebras" }),
538
654
  },
539
- {
540
- llm_model_name: "qwen3-235b-a22b-2507-thinking@cerebras",
541
- llm_model_code: "qwen/qwen3-235b-a22b-thinking-2507",
542
- llm_api_code: "openrouter",
543
- context_window: 262_144,
544
- cents_input: 60,
545
- cents_output: 120,
546
- default_reasoning: true,
547
- has_structured_json: true,
548
- recommended_temperature: undefined,
549
- provider_options: provider_options_openrouter({ only: "cerebras" }),
550
- },
551
655
  {
552
656
  llm_model_name: "qwen3-coder@alibaba",
553
657
  llm_model_code: "qwen/qwen3-coder",
554
658
  llm_api_code: "openrouter",
555
659
  context_window: 262_144,
556
- cents_input: 150,
557
- cents_output: 750,
660
+ max_output_tokens: 65_536,
661
+ cents_input: 150, // for input tokens <= 128K
662
+ cents_output: 750, // for input tokens <= 128K
558
663
  default_reasoning: false,
559
664
  has_structured_json: true,
560
665
  recommended_temperature: undefined,
561
666
  provider_options: provider_options_openrouter({ only: "alibaba/opensource" }),
562
667
  },
563
668
  {
564
- llm_model_name: "qwen3-coder@cerebras",
565
- llm_model_code: "qwen/qwen3-coder",
669
+ llm_model_name: "qwen-plus@alibaba",
670
+ llm_model_code: "qwen/qwen-plus-2025-07-28",
566
671
  llm_api_code: "openrouter",
567
- context_window: 131_072,
568
- cents_input: 200,
569
- cents_output: 200,
672
+ context_window: 1_000_000,
673
+ max_output_tokens: 32_768,
674
+ cents_input: 40, // for input tokens <= 256K
675
+ cents_output: 120, // for input tokens <= 256K
570
676
  default_reasoning: false,
571
677
  has_structured_json: true,
572
678
  recommended_temperature: undefined,
573
- provider_options: provider_options_openrouter({ only: "cerebras" }),
679
+ provider_options: provider_options_openrouter({ only: "alibaba" }),
574
680
  },
575
681
  ];
576
682
  export function llm_model_get_details({ llm_model_names, }) {
@@ -0,0 +1,109 @@
1
+ import { anyOf, createRegExp, digit, exactly, global, letter, oneOrMore, wordChar } from "magic-regexp";
2
+ import { ansi_yellow } from "./lib_ansi.js";
3
+ import { DASH } from "./lib_char_punctuation.js";
4
+ import { text_split_lines } from "./lib_text.js";
5
+ import { tui_confirm } from "./lib_tui_confirm.js";
6
+ import { tui_quote_smart_single } from "./lib_tui_quote.js";
7
+ const regexp_word_global = createRegExp(oneOrMore(anyOf(wordChar, exactly(DASH))), [global]);
8
+ const regexp_segment_global = createRegExp(oneOrMore(anyOf(letter, digit)), [global]);
9
+ const regexp_identifier_exactly = createRegExp(anyOf(
10
+ // Only letters (no digits)
11
+ oneOrMore(letter),
12
+ // Digits at the end
13
+ oneOrMore(letter).and(oneOrMore(digit)),
14
+ // Digits in the middle (letters before and after)
15
+ oneOrMore(letter)
16
+ .and(oneOrMore(digit))
17
+ .and(oneOrMore(letter)),
18
+ // Only digits (no letters)
19
+ oneOrMore(digit))
20
+ .at.lineStart()
21
+ .at.lineEnd());
22
+ function is_secret_line(line) {
23
+ if (line.endsWith(" # secret") || line.endsWith(" // secret")) {
24
+ return true;
25
+ }
26
+ return false;
27
+ }
28
+ const NOT_SECRET_LINE_INCLUDES = ["http://", "https://"];
29
+ function is_not_secret_line(line) {
30
+ if (line.endsWith(" not secret")) {
31
+ return true;
32
+ }
33
+ for (const not_secret_line_include of NOT_SECRET_LINE_INCLUDES) {
34
+ if (line.includes(not_secret_line_include)) {
35
+ return true;
36
+ }
37
+ }
38
+ return false;
39
+ }
40
+ const SECRET_WORD_REGEXPS = [
41
+ /^ghp_[A-Za-z0-9]{30}/, // GitHub Personal Access Token
42
+ /^glpat-[A-Za-z0-9]{20}/, // GitLab Personal Access Token
43
+ /^sk-[A-Za-z0-9-]{30}/, // Secret Key
44
+ /^sk_[A-Za-z0-9]{30}/, // Secret Key
45
+ /^sk_test_[A-Za-z0-9]{30}/, // Secret Key (test)
46
+ /^whsec_[A-Za-z0-9]{30}/, // WebHook Secret key
47
+ /^xox[a-z]-[A-Za-z0-9-]{27}/, // Slack Access Token
48
+ ];
49
+ function is_secret_word(word) {
50
+ for (const secret_word_regexp of SECRET_WORD_REGEXPS) {
51
+ if (secret_word_regexp.test(word)) {
52
+ return true;
53
+ }
54
+ }
55
+ return false;
56
+ }
57
+ async function is_secret_segment(segment, not_secret_segments, interactive) {
58
+ if (not_secret_segments.has(segment)) {
59
+ return false;
60
+ }
61
+ if (regexp_identifier_exactly.test(segment)) {
62
+ return false;
63
+ }
64
+ if (segment.length < 20) {
65
+ return false;
66
+ }
67
+ if (interactive) {
68
+ const confirmed_is_secret = await tui_confirm({
69
+ question: `Is ${tui_quote_smart_single(segment)} a secret?`,
70
+ default: false,
71
+ style_message: ansi_yellow,
72
+ });
73
+ if (!confirmed_is_secret) {
74
+ not_secret_segments.add(segment);
75
+ return false;
76
+ }
77
+ }
78
+ return true;
79
+ }
80
+ export async function secret_check({ text, interactive }) {
81
+ const not_secret_segments = new Set();
82
+ const lines = text_split_lines(text);
83
+ for (const line of lines.toReversed()) {
84
+ if (is_secret_line(line)) {
85
+ throw new Error(`Secret detected: ${tui_quote_smart_single(line.trim())}`);
86
+ }
87
+ const words = line.match(regexp_word_global);
88
+ const segments = line.match(regexp_segment_global);
89
+ if (!words || !segments) {
90
+ continue;
91
+ }
92
+ if (is_not_secret_line(line)) {
93
+ for (const segment of segments) {
94
+ not_secret_segments.add(segment);
95
+ }
96
+ continue;
97
+ }
98
+ for (const word of words) {
99
+ if (is_secret_word(word)) {
100
+ throw new Error(`Secret detected: ${tui_quote_smart_single(word)}`);
101
+ }
102
+ }
103
+ for (const segment of segments) {
104
+ if (await is_secret_segment(segment, not_secret_segments, interactive)) {
105
+ throw new Error(`Secret detected: ${tui_quote_smart_single(segment)}`);
106
+ }
107
+ }
108
+ }
109
+ }
@@ -0,0 +1,39 @@
1
+ import { LF } from "./lib_char_control.js";
2
+ import { EMPTY } from "./lib_char_empty.js";
3
+ export function text_split_lines(text) {
4
+ const lines = text.split(/\r?\n/);
5
+ if (lines.length > 0) {
6
+ const last_line = lines.at(-1);
7
+ if (last_line === EMPTY) {
8
+ return lines.slice(0, -1);
9
+ }
10
+ }
11
+ return lines;
12
+ }
13
+ export function text_join_lines(lines) {
14
+ return lines.length > 0 ? lines.join(LF) + LF : EMPTY;
15
+ }
16
+ function text_lines_matching_generic(text, pattern, remove) {
17
+ const regex = new RegExp(pattern);
18
+ const lines = text_split_lines(text);
19
+ const new_lines = [];
20
+ for (const line of lines) {
21
+ const found = regex.test(line);
22
+ if (found !== remove) {
23
+ new_lines.push(line);
24
+ }
25
+ }
26
+ return text_join_lines(new_lines);
27
+ }
28
+ export function text_lines_matching_only(text, pattern) {
29
+ return text_lines_matching_generic(text, pattern, false);
30
+ }
31
+ export function text_lines_matching_remove(text, pattern) {
32
+ return text_lines_matching_generic(text, pattern, true);
33
+ }
34
+ export function text_get_head(text, lines) {
35
+ return text_join_lines(text_split_lines(text).slice(0, lines));
36
+ }
37
+ export function text_get_tail(text, lines) {
38
+ return text_join_lines(text_split_lines(text).slice(-lines));
39
+ }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@johnowennixon/diffdash",
3
- "version": "1.9.0",
3
+ "version": "1.11.0",
4
4
  "description": "A command-line tool to generate Git commit messages using AI",
5
5
  "license": "0BSD",
6
6
  "author": "John Owen Nixon",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/johnowennixon/diffdash.git"
9
+ "url": "git+https://github.com/johnowennixon/diffdash.git"
10
10
  },
11
11
  "engines": {
12
12
  "node": ">=20"
@@ -19,34 +19,35 @@
19
19
  "diffdash": "dist/src/diffdash.js"
20
20
  },
21
21
  "dependencies": {
22
- "@ai-sdk/anthropic": "2.0.9",
23
- "@ai-sdk/deepseek": "1.0.13",
24
- "@ai-sdk/google": "2.0.11",
25
- "@ai-sdk/openai": "2.0.23",
26
- "@inquirer/prompts": "7.8.4",
27
- "@openrouter/ai-sdk-provider": "1.1.2",
28
- "ai": "5.0.29",
29
- "ansis": "4.1.0",
22
+ "@ai-sdk/anthropic": "2.0.53",
23
+ "@ai-sdk/deepseek": "1.0.31",
24
+ "@ai-sdk/google": "2.0.44",
25
+ "@ai-sdk/openai": "2.0.77",
26
+ "@inquirer/prompts": "8.0.2",
27
+ "@openrouter/ai-sdk-provider": "1.2.3",
28
+ "ai": "5.0.102",
29
+ "ansis": "4.2.0",
30
30
  "argparse": "2.0.1",
31
31
  "cli-table3": "0.6.5",
32
32
  "json5": "2.2.3",
33
- "simple-git": "3.28.0",
34
- "zod": "4.1.5"
33
+ "magic-regexp": "0.10.0",
34
+ "simple-git": "3.30.0",
35
+ "zod": "4.1.13"
35
36
  },
36
37
  "devDependencies": {
37
- "@biomejs/biome": "2.2.2",
38
- "@candide/tsgolint": "1.3.0",
38
+ "@biomejs/biome": "2.3.8",
39
+ "@candide/tsgolint": "1.4.0",
39
40
  "@johnowennixon/add-shebangs": "1.1.0",
40
41
  "@johnowennixon/chmodx": "2.1.0",
41
42
  "@types/argparse": "2.0.17",
42
- "@types/node": "24.3.0",
43
- "@typescript/native-preview": "7.0.0-dev.20250902.1",
44
- "knip": "5.63.0",
45
- "markdownlint-cli2": "0.18.1",
43
+ "@types/node": "24.10.1",
44
+ "@typescript/native-preview": "7.0.0-dev.20251022.1",
45
+ "knip": "5.71.0",
46
+ "markdownlint-cli2": "0.19.1",
46
47
  "npm-run-all2": "8.0.4",
47
- "oxlint": "1.14.0",
48
- "rimraf": "6.0.1",
49
- "typescript": "5.9.2"
48
+ "oxlint": "1.31.0",
49
+ "rimraf": "6.1.2",
50
+ "typescript": "5.9.3"
50
51
  },
51
52
  "scripts": {
52
53
  "build": "run-s -ls build:clean build:tsc build:shebang build:chmod",