Compare commits
9 Commits
fix/repl-a
...
fix/363-st
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af5bb8fed8 | ||
|
|
ad76b1174a | ||
|
|
c457d9db3c | ||
|
|
d1f79088a1 | ||
|
|
106f85d0bf | ||
|
|
3188f6ac66 | ||
|
|
69ea1f1e4a | ||
|
|
f9ce81bfb3 | ||
|
|
4975cfc2e0 |
@@ -68,11 +68,11 @@ When a user describes what they want an agent to do, you will:
|
||||
assistant: "Now let me use the test-runner agent to run the tests"
|
||||
</example>
|
||||
- <example>
|
||||
Context: User is creating an agent to respond to the word "hello" with a friendly jok.
|
||||
user: "Hello"
|
||||
assistant: "I'm going to use the ${AGENT_TOOL_NAME} tool to launch the greeting-responder agent to respond with a friendly joke"
|
||||
Context: User is creating an agent for Claude Code product questions.
|
||||
user: "How do I configure Claude Code hooks?"
|
||||
assistant: "I'm going to use the ${AGENT_TOOL_NAME} tool to launch the claude-code-guide agent to answer the question"
|
||||
<commentary>
|
||||
Since the user is greeting, use the greeting-responder agent to respond with a friendly joke.
|
||||
Since the user is asking how to use Claude Code, use the claude-code-guide agent.
|
||||
</commentary>
|
||||
</example>
|
||||
- If the user mentioned or implied that the agent should be used proactively, you should include examples of this.
|
||||
|
||||
@@ -238,6 +238,7 @@ import { usePromptsFromClaudeInChrome } from 'src/hooks/usePromptsFromClaudeInCh
|
||||
import { getTipToShowOnSpinner, recordShownTip } from 'src/services/tips/tipScheduler.js';
|
||||
import type { Theme } from 'src/utils/theme.js';
|
||||
import { isPromptTypingSuppressionActive } from './replInputSuppression.js';
|
||||
import { shouldRunStartupChecks } from './replStartupGates.js';
|
||||
import { checkAndDisableBypassPermissionsIfNeeded, checkAndDisableAutoModeIfNeeded, useKickOffCheckAndDisableBypassPermissionsIfNeeded, useKickOffCheckAndDisableAutoModeIfNeeded } from 'src/utils/permissions/bypassPermissionsKillswitch.js';
|
||||
import { SandboxManager } from 'src/utils/sandbox/sandbox-adapter.js';
|
||||
import { SANDBOX_NETWORK_ACCESS_TOOL_NAME } from 'src/cli/structuredIO.js';
|
||||
@@ -792,10 +793,8 @@ export function REPL({
|
||||
// accepts, and only then is the REPL component mounted and this effect runs.
|
||||
// This ensures that plugin installations from repository and user settings only
|
||||
// happen after explicit user consent to trust the current working directory.
|
||||
useEffect(() => {
|
||||
if (isRemoteSession) return;
|
||||
void performStartupChecks(setAppState);
|
||||
}, [setAppState, isRemoteSession]);
|
||||
// Deferring startup checks is handled below (after promptTypingSuppressionActive
|
||||
// is declared) to avoid temporal dead zone issues.
|
||||
|
||||
// Allow Claude in Chrome MCP to send prompts through MCP notifications
|
||||
// and sync permission mode changes to the Chrome extension
|
||||
@@ -1429,6 +1428,25 @@ export function REPL({
|
||||
const activeRemote = sshRemote.isRemoteMode ? sshRemote : directConnect.isRemoteMode ? directConnect : remoteSession;
|
||||
const [pastedContents, setPastedContents] = useState<Record<number, PastedContent>>({});
|
||||
const [submitCount, setSubmitCount] = useState(0);
|
||||
|
||||
// Defer startup checks until the user has submitted their first message.
|
||||
// A timeout or grace period is insufficient (issue #363): if the user pauses
|
||||
// before typing, startup checks can still fire and recommendation dialogs
|
||||
// steal focus. Only the user's first submission guarantees the prompt was
|
||||
// the first thing they interacted with.
|
||||
const startupChecksStartedRef = React.useRef(false);
|
||||
const hasHadFirstSubmission = (submitCount ?? 0) > 0;
|
||||
useEffect(() => {
|
||||
if (isRemoteSession) return;
|
||||
if (startupChecksStartedRef.current) return;
|
||||
if (!shouldRunStartupChecks({
|
||||
isRemoteSession,
|
||||
hasStarted: startupChecksStartedRef.current,
|
||||
hasHadFirstSubmission,
|
||||
})) return;
|
||||
startupChecksStartedRef.current = true;
|
||||
void performStartupChecks(setAppState);
|
||||
}, [setAppState, isRemoteSession, hasHadFirstSubmission]);
|
||||
// Ref instead of state to avoid triggering React re-renders on every
|
||||
// streaming text_delta. The spinner reads this via its animation timer.
|
||||
const responseLengthRef = useRef(0);
|
||||
@@ -2061,13 +2079,14 @@ export function REPL({
|
||||
if (allowDialogsWithAnimation && showRemoteCallout) return 'remote-callout';
|
||||
|
||||
// LSP plugin recommendation (lowest priority - non-blocking suggestion)
|
||||
if (allowDialogsWithAnimation && lspRecommendation) return 'lsp-recommendation';
|
||||
// Suppress during startup window to prevent stealing focus from the prompt (issue #363)
|
||||
if (allowDialogsWithAnimation && lspRecommendation && startupChecksStartedRef.current) return 'lsp-recommendation';
|
||||
|
||||
// Plugin hint from CLI/SDK stderr (same priority band as LSP rec)
|
||||
if (allowDialogsWithAnimation && hintRecommendation) return 'plugin-hint';
|
||||
if (allowDialogsWithAnimation && hintRecommendation && startupChecksStartedRef.current) return 'plugin-hint';
|
||||
|
||||
// Desktop app upsell (max 3 launches, lowest priority)
|
||||
if (allowDialogsWithAnimation && showDesktopUpsellStartup) return 'desktop-upsell';
|
||||
if (allowDialogsWithAnimation && showDesktopUpsellStartup && startupChecksStartedRef.current) return 'desktop-upsell';
|
||||
return undefined;
|
||||
}
|
||||
const focusedInputDialog = getFocusedInputDialog();
|
||||
|
||||
53
src/screens/replStartupGates.test.ts
Normal file
53
src/screens/replStartupGates.test.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { shouldRunStartupChecks } from './replStartupGates.js'
|
||||
|
||||
describe('shouldRunStartupChecks', () => {
|
||||
test('runs checks after first message submission', () => {
|
||||
expect(shouldRunStartupChecks({
|
||||
isRemoteSession: false,
|
||||
hasStarted: false,
|
||||
hasHadFirstSubmission: true,
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
test('skips checks in remote sessions even after submission', () => {
|
||||
expect(shouldRunStartupChecks({
|
||||
isRemoteSession: true,
|
||||
hasStarted: false,
|
||||
hasHadFirstSubmission: true,
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('skips checks if already started', () => {
|
||||
expect(shouldRunStartupChecks({
|
||||
isRemoteSession: false,
|
||||
hasStarted: true,
|
||||
hasHadFirstSubmission: true,
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('does not run checks before first submission', () => {
|
||||
expect(shouldRunStartupChecks({
|
||||
isRemoteSession: false,
|
||||
hasStarted: false,
|
||||
hasHadFirstSubmission: false,
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('does not run checks when idle before first submission', () => {
|
||||
expect(shouldRunStartupChecks({
|
||||
isRemoteSession: false,
|
||||
hasStarted: false,
|
||||
hasHadFirstSubmission: false,
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('skips checks in remote session regardless of other conditions', () => {
|
||||
expect(shouldRunStartupChecks({
|
||||
isRemoteSession: true,
|
||||
hasStarted: false,
|
||||
hasHadFirstSubmission: false,
|
||||
})).toBe(false)
|
||||
})
|
||||
})
|
||||
35
src/screens/replStartupGates.ts
Normal file
35
src/screens/replStartupGates.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Startup gates for the REPL.
|
||||
*
|
||||
* Prevents startup plugin checks and recommendation dialogs from stealing
|
||||
* focus before the user has interacted with the prompt.
|
||||
*
|
||||
* This addresses the root cause of issue #363: on mount, performStartupChecks
|
||||
* triggers plugin loading, which populates trackedFiles, which triggers
|
||||
* useLspPluginRecommendation to surface an LSP recommendation dialog. Since
|
||||
* promptTypingSuppressionActive is false before the user has typed anything,
|
||||
* getFocusedInputDialog() returns the dialog, unmounting PromptInput entirely.
|
||||
*
|
||||
* The fix gates startup checks on actual prompt interaction. A pure timeout
|
||||
* or grace period is insufficient because pausing before typing would still
|
||||
* allow dialogs to steal focus. Only the user's first submission guarantees
|
||||
* the prompt is no longer in the vulnerable pre-interaction window.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Determines whether startup checks should run.
|
||||
*
|
||||
* Startup checks are deferred until the user has submitted their first
|
||||
* message. This guarantees the prompt was the first thing the user interacted
|
||||
* with, so no recommendation dialog can steal focus before the first keystroke.
|
||||
*/
|
||||
export function shouldRunStartupChecks(options: {
|
||||
isRemoteSession: boolean;
|
||||
hasStarted: boolean;
|
||||
hasHadFirstSubmission: boolean;
|
||||
}): boolean {
|
||||
if (options.isRemoteSession) return false;
|
||||
if (options.hasStarted) return false;
|
||||
if (!options.hasHadFirstSubmission) return false;
|
||||
return true;
|
||||
}
|
||||
33
src/services/tools/toolExecution.test.ts
Normal file
33
src/services/tools/toolExecution.test.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { SkillTool } from '../../tools/SkillTool/SkillTool.js'
|
||||
import {
|
||||
getSchemaValidationErrorOverride,
|
||||
getSchemaValidationToolUseResult,
|
||||
} from './toolExecution.js'
|
||||
|
||||
describe('getSchemaValidationErrorOverride', () => {
|
||||
test('returns actionable missing-skill error for SkillTool', () => {
|
||||
expect(getSchemaValidationErrorOverride(SkillTool, {})).toBe(
|
||||
'Missing skill name. Pass the slash command name as the skill parameter (e.g., skill: "commit" for /commit, skill: "review-pr" for /review-pr).',
|
||||
)
|
||||
})
|
||||
|
||||
test('does not override unrelated tool schema failures', () => {
|
||||
expect(getSchemaValidationErrorOverride({ name: 'Read' } as never, {})).toBe(
|
||||
null,
|
||||
)
|
||||
})
|
||||
|
||||
test('does not override SkillTool when skill is present', () => {
|
||||
expect(
|
||||
getSchemaValidationErrorOverride(SkillTool, { skill: 'commit' }),
|
||||
).toBe(null)
|
||||
})
|
||||
|
||||
test('uses the actionable override for structured toolUseResult too', () => {
|
||||
expect(getSchemaValidationToolUseResult(SkillTool, {} as never)).toBe(
|
||||
'InputValidationError: Missing skill name. Pass the slash command name as the skill parameter (e.g., skill: "commit" for /commit, skill: "review-pr" for /review-pr).',
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -43,6 +43,7 @@ import { FILE_READ_TOOL_NAME } from '../../tools/FileReadTool/prompt.js'
|
||||
import { FILE_WRITE_TOOL_NAME } from '../../tools/FileWriteTool/prompt.js'
|
||||
import { NOTEBOOK_EDIT_TOOL_NAME } from '../../tools/NotebookEditTool/constants.js'
|
||||
import { POWERSHELL_TOOL_NAME } from '../../tools/PowerShellTool/toolName.js'
|
||||
import { SKILL_TOOL_NAME } from '../../tools/SkillTool/constants.js'
|
||||
import { parseGitCommitId } from '../../tools/shared/gitOperationTracking.js'
|
||||
import {
|
||||
isDeferredTool,
|
||||
@@ -596,6 +597,31 @@ export function buildSchemaNotSentHint(
|
||||
)
|
||||
}
|
||||
|
||||
export function getSchemaValidationErrorOverride(
|
||||
tool: Tool,
|
||||
input: unknown,
|
||||
): string | null {
|
||||
if (tool.name !== SKILL_TOOL_NAME || !input || typeof input !== 'object') {
|
||||
return null
|
||||
}
|
||||
|
||||
const skill = (input as { skill?: unknown }).skill
|
||||
if (skill === undefined || skill === null) {
|
||||
return 'Missing skill name. Pass the slash command name as the skill parameter (e.g., skill: "commit" for /commit, skill: "review-pr" for /review-pr).'
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function getSchemaValidationToolUseResult(
|
||||
tool: Tool,
|
||||
input: unknown,
|
||||
fallbackMessage?: string,
|
||||
): string {
|
||||
const override = getSchemaValidationErrorOverride(tool, input)
|
||||
return `InputValidationError: ${override ?? fallbackMessage ?? ''}`
|
||||
}
|
||||
|
||||
async function checkPermissionsAndCallTool(
|
||||
tool: Tool,
|
||||
toolUseID: string,
|
||||
@@ -614,7 +640,9 @@ async function checkPermissionsAndCallTool(
|
||||
// Validate input types with zod (surprisingly, the model is not great at generating valid input)
|
||||
const parsedInput = tool.inputSchema.safeParse(input)
|
||||
if (!parsedInput.success) {
|
||||
let errorContent = formatZodValidationError(tool.name, parsedInput.error)
|
||||
const fallbackErrorContent = formatZodValidationError(tool.name, parsedInput.error)
|
||||
let errorContent =
|
||||
getSchemaValidationErrorOverride(tool, input) ?? fallbackErrorContent
|
||||
|
||||
const schemaHint = buildSchemaNotSentHint(
|
||||
tool,
|
||||
@@ -672,7 +700,11 @@ async function checkPermissionsAndCallTool(
|
||||
tool_use_id: toolUseID,
|
||||
},
|
||||
],
|
||||
toolUseResult: `InputValidationError: ${parsedInput.error.message}`,
|
||||
toolUseResult: getSchemaValidationToolUseResult(
|
||||
tool,
|
||||
input,
|
||||
parsedInput.error.message,
|
||||
),
|
||||
sourceToolAssistantUUID: assistantMessage.uuid,
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -156,34 +156,24 @@ ${AGENT_TOOL_NAME}({
|
||||
const currentExamples = `Example usage:
|
||||
|
||||
<example_agent_descriptions>
|
||||
"test-runner": use this agent after you are done writing code to run tests
|
||||
"greeting-responder": use this agent to respond to user greetings with a friendly joke
|
||||
"claude-code-guide": use this agent when the user asks how Claude Code works or how to use its features
|
||||
"statusline-setup": use this agent to configure the user's Claude Code status line setting
|
||||
</example_agent_descriptions>
|
||||
|
||||
<example>
|
||||
user: "Please write a function that checks if a number is prime"
|
||||
assistant: I'm going to use the ${FILE_WRITE_TOOL_NAME} tool to write the following code:
|
||||
<code>
|
||||
function isPrime(n) {
|
||||
if (n <= 1) return false
|
||||
for (let i = 2; i * i <= n; i++) {
|
||||
if (n % i === 0) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
</code>
|
||||
user: "How do I configure Claude Code hooks?"
|
||||
<commentary>
|
||||
Since a significant piece of code was written and the task was completed, now use the test-runner agent to run the tests
|
||||
This is a Claude Code usage question, so use the claude-code-guide agent
|
||||
</commentary>
|
||||
assistant: Uses the ${AGENT_TOOL_NAME} tool to launch the test-runner agent
|
||||
assistant: Uses the ${AGENT_TOOL_NAME} tool to launch the claude-code-guide agent
|
||||
</example>
|
||||
|
||||
<example>
|
||||
user: "Hello"
|
||||
user: "Set up my Claude Code status line"
|
||||
<commentary>
|
||||
Since the user is greeting, use the greeting-responder agent to respond with a friendly joke
|
||||
This matches the statusline-setup agent, so use it to configure the setting
|
||||
</commentary>
|
||||
assistant: "I'm going to use the ${AGENT_TOOL_NAME} tool to launch the greeting-responder agent"
|
||||
assistant: "I'm going to use the ${AGENT_TOOL_NAME} tool to launch the statusline-setup agent"
|
||||
</example>
|
||||
`
|
||||
|
||||
|
||||
31
src/tools/SkillTool/SkillTool.test.ts
Normal file
31
src/tools/SkillTool/SkillTool.test.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { SkillTool } from './SkillTool.js'
|
||||
|
||||
describe('SkillTool missing parameter handling', () => {
|
||||
test('missing skill stays required at the schema level', async () => {
|
||||
const parsed = SkillTool.inputSchema.safeParse({})
|
||||
|
||||
expect(parsed.success).toBe(false)
|
||||
})
|
||||
|
||||
test('validateInput still returns an actionable error when called with missing skill', async () => {
|
||||
const result = await SkillTool.validateInput?.({} as never, {
|
||||
options: { tools: [] },
|
||||
messages: [],
|
||||
} as never)
|
||||
|
||||
expect(result).toEqual({
|
||||
result: false,
|
||||
message:
|
||||
'Missing skill name. Pass the slash command name as the skill parameter (e.g., skill: "commit" for /commit, skill: "review-pr" for /review-pr).',
|
||||
errorCode: 1,
|
||||
})
|
||||
})
|
||||
|
||||
test('valid skill input still parses and validates', async () => {
|
||||
const parsed = SkillTool.inputSchema.safeParse({ skill: 'commit' })
|
||||
|
||||
expect(parsed.success).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -352,6 +352,16 @@ export const SkillTool: Tool<InputSchema, Output, Progress> = buildTool({
|
||||
toAutoClassifierInput: ({ skill }) => skill ?? '',
|
||||
|
||||
async validateInput({ skill }, context): Promise<ValidationResult> {
|
||||
if (!skill || typeof skill !== 'string') {
|
||||
return {
|
||||
result: false,
|
||||
message:
|
||||
'Missing skill name. Pass the slash command name as the skill parameter ' +
|
||||
'(e.g., skill: "commit" for /commit, skill: "review-pr" for /review-pr).',
|
||||
errorCode: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// Skills are just skill names, no arguments
|
||||
const trimmed = skill.trim()
|
||||
if (!trimmed) {
|
||||
@@ -434,7 +444,7 @@ export const SkillTool: Tool<InputSchema, Output, Progress> = buildTool({
|
||||
context,
|
||||
): Promise<PermissionDecision> {
|
||||
// Skills are just skill names, no arguments
|
||||
const trimmed = skill.trim()
|
||||
const trimmed = skill ?? ''
|
||||
|
||||
// Remove leading slash if present (for compatibility)
|
||||
const commandName = trimmed.startsWith('/') ? trimmed.substring(1) : trimmed
|
||||
@@ -592,7 +602,7 @@ export const SkillTool: Tool<InputSchema, Output, Progress> = buildTool({
|
||||
// - Skill is a prompt-based skill
|
||||
|
||||
// Skills are just names, with optional arguments
|
||||
const trimmed = skill.trim()
|
||||
const trimmed = skill ?? ''
|
||||
|
||||
// Remove leading slash if present (for compatibility)
|
||||
const commandName = trimmed.startsWith('/') ? trimmed.substring(1) : trimmed
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect, test } from 'bun:test'
|
||||
import { z } from 'zod/v4'
|
||||
import { getEmptyToolPermissionContext, type Tool, type Tools } from '../Tool.js'
|
||||
import { SkillTool } from '../tools/SkillTool/SkillTool.js'
|
||||
import { toolToAPISchema } from './api.js'
|
||||
|
||||
test('toolToAPISchema preserves provider-specific schema keywords in input_schema', async () => {
|
||||
@@ -64,3 +65,16 @@ test('toolToAPISchema preserves provider-specific schema keywords in input_schem
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('toolToAPISchema keeps skill required for SkillTool', async () => {
|
||||
const schema = await toolToAPISchema(SkillTool, {
|
||||
getToolPermissionContext: async () => getEmptyToolPermissionContext(),
|
||||
tools: [] as unknown as Tools,
|
||||
agents: [],
|
||||
})
|
||||
|
||||
expect((schema as { input_schema: unknown }).input_schema).toMatchObject({
|
||||
type: 'object',
|
||||
required: ['skill'],
|
||||
})
|
||||
})
|
||||
|
||||
@@ -94,3 +94,22 @@ test('gpt-5.4 family keeps large max output overrides within provider limits', (
|
||||
expect(getMaxOutputTokensForModel('gpt-5.4-mini')).toBe(128_000)
|
||||
expect(getMaxOutputTokensForModel('gpt-5.4-nano')).toBe(128_000)
|
||||
})
|
||||
|
||||
test('MiniMax-M2.7 uses explicit provider-specific context and output caps', () => {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
delete process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS
|
||||
|
||||
expect(getContextWindowForModel('MiniMax-M2.7')).toBe(204_800)
|
||||
expect(getModelMaxOutputTokens('MiniMax-M2.7')).toEqual({
|
||||
default: 131_072,
|
||||
upperLimit: 131_072,
|
||||
})
|
||||
expect(getMaxOutputTokensForModel('MiniMax-M2.7')).toBe(131_072)
|
||||
})
|
||||
|
||||
test('unknown openai-compatible models still use the conservative fallback window', () => {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
delete process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS
|
||||
|
||||
expect(getContextWindowForModel('some-unknown-3p-model')).toBe(8_000)
|
||||
})
|
||||
|
||||
@@ -74,7 +74,7 @@ export function getContextWindowForModel(
|
||||
|
||||
// OpenAI-compatible provider — use known context windows for the model.
|
||||
// Unknown models get a conservative 8k default so auto-compact triggers
|
||||
// before hitting a hard context_window_exceeded error (issue #248 finding 3).
|
||||
// before hitting a hard context_window_exceeded error.
|
||||
const isOpenAIProvider =
|
||||
isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI) ||
|
||||
isEnvTruthy(process.env.CLAUDE_CODE_USE_GEMINI) ||
|
||||
|
||||
@@ -44,6 +44,10 @@ const OPENAI_CONTEXT_WINDOWS: Record<string, number> = {
|
||||
'mistral-large-latest': 131_072,
|
||||
'mistral-small-latest': 131_072,
|
||||
|
||||
// MiniMax
|
||||
'MiniMax-M2.7': 204_800,
|
||||
'minimax-m2.7': 204_800,
|
||||
|
||||
// Google (via OpenRouter)
|
||||
'google/gemini-2.0-flash':1_048_576,
|
||||
'google/gemini-2.5-pro': 1_048_576,
|
||||
@@ -110,6 +114,10 @@ const OPENAI_MAX_OUTPUT_TOKENS: Record<string, number> = {
|
||||
'mistral-large-latest': 32_768,
|
||||
'mistral-small-latest': 32_768,
|
||||
|
||||
// MiniMax
|
||||
'MiniMax-M2.7': 131_072,
|
||||
'minimax-m2.7': 131_072,
|
||||
|
||||
// Google (via OpenRouter)
|
||||
'google/gemini-2.0-flash': 8_192,
|
||||
'google/gemini-2.5-pro': 65_536,
|
||||
|
||||
Reference in New Issue
Block a user