Files
claudecodeui/docs/backend/llm-unifier-helper.md

13 KiB

How each session processes sessions

  • The way each session processes the sessions is already setup in server/src/modules/providers. Port over the existing logic to the new classes if possible.

How to start, resume, and stop a session

Claude

A new session is created by calling query({ prompt, options }) which yields an async stream of SDK messages. The session ID can be provided explicitly by using resume option and passing the session id (sdkOptions.resume = sessionId;).

https://platform.claude.com/docs/en/agent-sdk/typescript#types

Session can be stopped midway using queryInstance.interrupt() https://platform.claude.com/docs/en/agent-sdk/typescript#methods

Codex

  • Starting - const thread = codex.startThread(threadOptions)
  • Resuming - codex.resumeThread(sessionId, threadOptions);
  • Stop a session
        // Execute with streaming
    
    

    const streamedTurn = await thread.runStreamed(command, {

      signal: abortController.signal

    }); ```

About Abort controllers

  • Think of AbortController as a cancel button for async work.
  • Controller = thing that sends the cancel command.
  • Signal = thing that receives or carries the cancel state
const controller = new AbortController();

fetch("https://api.example.com/data", {
  signal: controller.signal
})
  .then(response => response.json())
  .then(data => {
    console.log("Finished:", data);
  })
  .catch(error => {
    if (error.name === "AbortError") {
      console.log("The request was cancelled");
    } else {
      console.error("Real error:", error);
    }
  });

// Cancel it after 2 seconds
setTimeout(() => {
  controller.abort();
}, 2000);
  • AbortController does not magically stop all JavaScript everywhere. It only works if the API or function you are using actually supports cancellation via a signal. fetch does. Your own custom async functions can too, but you have to write that support yourself. In codex, the method runStreamed supports it as well.
function wait(ms, { signal } = {}) {
  return new Promise((resolve, reject) => {
  // if signal was aborted EVEN BEFORE the function started, return back.
  // This catches the case where someone did this first:
  // controller.abort("Cancelled already");
  // wait(5000, { signal: controller.signal });
    if (signal?.aborted) {
      reject(signal.reason); // it supports custom reasoning as well.
      return;
    }

    const timeoutId = setTimeout(() => {
      resolve("Done waiting");
    }, ms);
	
	// when the signal.abort event is fired (when controller.abort() is called somewhere else), it sends an `abort` event.
	// When we get this, remove the timeoutId
    signal?.addEventListener("abort", () => {
      clearTimeout(timeoutId);
      reject(signal.reason);
    });
  });
}


// ---------------- USAGE --------------------
const controller = new AbortController();

wait(5000, { signal: controller.signal })
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.log("Cancelled:", error);
  });

setTimeout(() => {
  controller.abort("User cancelled the wait");
}, 1000);

Gemini

Start

spawn gemini --prompt "actualprompt" --model "actual model", --output-format 'stream-json'

  • Stream json output format send responses in terms of a series of json chunks. If we store it, we would use .jsonl format.
  • Allowed tools aren't needed as it's depreciated.
      --allowed-tools             [DEPRECATED: Use Policy Engine instead See
                                  https://geminicli.com/docs/core/policy-engine] Tools that are allowed 
                                  to run without confirmation     
  • --prompt allows us to run just one prompt in headless mode. It will automatically trust the workspace directory so it won't ask us whether we trust the workspace or not.

Stop/Abort a session

try {
	geminiProc.kill('SIGTERM'); // gracefully terminates the process. It ASKS the process to shut down cleanly. The process can catch it, save state, close files, and exit
	setTimeout(() => {
			geminiProc.kill('SIGKILL'); // kills it immediately
		}
	}, 2000); // Wait 2 seconds before force kill

	return true;
} catch (error) {
	return false;
}

resume

  • spawn gemini <the above formats> --resume <sessionId>

To receive a response

child.stdout.on('data', (chunk) => {
	const text = chunk.toString();
	...
})

child.stderr.on('data', (chunk) => {
  const text = chunk.toString();
  ...
}

Cursor

Start

  • spawn cursor-agent --print --trust --output-format 'stream-json' <actual-prompt'> This won't be able to run shell commands like git init. To be able to run those, --yolo must be passed.

Resume

  • spawn cursor-agent <above commands> --resume <sessionID>

abort

  • same approach as gemini.

How to fetch (list the model types supported for each model...find out if there is an easy way to fetch automatically from the files)

Claude

query.supportedModels() returns ModelInfo[].

/**
 * Information about an available model.
 */
export declare type ModelInfo = {
    /**
     * Model identifier to use in API calls
     */
    value: string;
    /**
     * Human-readable display name
     */
    displayName: string;
    /**
     * Description of the model's capabilities
     */
    description: string;
    /**
     * Whether this model supports effort levels
     */
    supportsEffort?: boolean;
    /**
     * Available effort levels for this model
     */
    supportedEffortLevels?: ('low' | 'medium' | 'high' | 'max')[];
    /**
     * Whether this model supports adaptive thinking (Claude decides when and how much to think)
     */
    supportsAdaptiveThinking?: boolean;
};
supported models = [
  {
    value: 'default',
    displayName: 'Default (recommended)',
    description: 'Use the default model (currently Sonnet 4.6) · $3/$15 per Mtok',
    supportsEffort: true,
    supportedEffortLevels: [ 'low', 'medium', 'high', 'max' ],
    supportsAdaptiveThinking: true
  },
  {
    value: 'sonnet[1m]',
    displayName: 'Sonnet (1M context)',
    description: 'Sonnet 4.6 for long sessions · $6/$22.50 per Mtok',
    supportsEffort: true,
    supportedEffortLevels: [ 'low', 'medium', 'high', 'max' ],
    supportsAdaptiveThinking: true
  },
  {
    value: 'opus',
    displayName: 'Opus',
    description: 'Opus 4.6 · Most capable for complex work · $5/$25 per Mtok',
    supportsEffort: true,
    supportedEffortLevels: [ 'low', 'medium', 'high', 'max' ],
    supportsAdaptiveThinking: true
  },
  {
    value: 'opus[1m]',
    displayName: 'Opus (1M context)',
    description: 'Opus 4.6 for long sessions · $10/$37.50 per Mtok',
    supportsEffort: true,
    supportedEffortLevels: [ 'low', 'medium', 'high', 'max' ],
    supportsAdaptiveThinking: true
  },
  {
    value: 'haiku',
    displayName: 'Haiku',
    description: 'Haiku 4.5 · Fastest for quick answers · $1/$5 per Mtok'
  },
  {
    value: 'sonnet',
    displayName: 'sonnet',
    description: 'Custom model',
    supportsEffort: true,
    supportedEffortLevels: [ 'low', 'medium', 'high', 'max' ],
    supportsAdaptiveThinking: true
  }
]

Codex

  • Found in .codex/models_cache.json. It's in the models attribute.
{
	...,
	"models": [
		    {
      "slug": "gpt-5.4",
      "display_name": "gpt-5.4",
      "description": "Latest frontier agentic coding model.",
      "default_reasoning_level": "medium",
      "supported_reasoning_levels": [
        {
          "effort": "low",
          "description": "Fast responses with lighter reasoning"
        },
        {
          "effort": "medium",
          "description": "Balances speed and reasoning depth for everyday tasks"
        },
        {
          "effort": "high",
          "description": "Greater reasoning depth for complex problems"
        },
        {
          "effort": "xhigh",
          "description": "Extra high reasoning depth for complex problems"
        }
      ],
      "shell_type": "shell_command",
      "visibility": "list",
      "supported_in_api": true,
      "priority": 1,
      "availability_nux": null,
      "upgrade": null,
      "base_instructions": "...",
      "model_messages": {
        "instructions_template": "...",
        "instructions_variables": {
          "personality_default": "",
          "personality_friendly": "..."
        }
      },
      "supports_reasoning_summaries": true,
      "default_reasoning_summary": "none",
      "support_verbosity": true,
      "default_verbosity": "low",
      "apply_patch_tool_type": "freeform",
      "web_search_tool_type": "text_and_image",
      "truncation_policy": {
        "mode": "tokens",
        "limit": 10000
      },
      "supports_parallel_tool_calls": true,
      "supports_image_detail_original": true,
      "context_window": 272000,
      "effective_context_window_percent": 95,
      "experimental_supported_tools": [],
      "input_modalities": [
        "text",
        "image"
      ],
      "supports_search_tool": true
    },
    {
	    ...
    }
	]
}

Gemini

  • There is no way to automatically do this. So, use this !Pasted image 20260401124033.png

The above is for free one. The below contains for all.

  OPTIONS: [
    { value: 'gemini-3.1-pro-preview', label: 'Gemini 3.1 Pro Preview' },
    { value: 'gemini-3-pro-preview', label: 'Gemini 3 Pro Preview' },
    { value: 'gemini-3-flash-preview', label: 'Gemini 3 Flash Preview' },
    { value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' },
    { value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' },
    { value: 'gemini-2.0-flash-lite', label: 'Gemini 2.0 Flash Lite' },
    { value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash' },
    { value: 'gemini-2.0-pro-exp', label: 'Gemini 2.0 Pro Experimental' },
    { value: 'gemini-2.0-flash-thinking-exp', label: 'Gemini 2.0 Flash Thinking' }
  ],

Cursor

  • spawn cursor-agent --list-models and parse the ANSI output.
function parseModelLine(line) {
  const trimmed = line.trim();

  if (!trimmed || trimmed === 'Available models' || trimmed.startsWith('Loading models') || trimmed.startsWith('Tip:')) {
    return null;
  }

  const match = trimmed.match(/^(.+?)\s+-\s+(.+)$/);
  if (!match) {
    return null;
  }

  const name = match[1].trim();
  let description = match[2].trim();
  const current = /\(current\)/i.test(description);
  const defaultModel = /\(default\)/i.test(description);

  description = description.replace(/\s*\((current|default)\)/gi, '').replace(/\s{2,}/g, ' ').trim();

  return {
    name,
    description,
    current,
    default: defaultModel,
  };
}

function parseModelsOutput(text) {
  const models = [];

  for (const line of stripAnsi(text).split(/\r?\n/)) {
    const parsed = parseModelLine(line);
    if (parsed) {
      models.push(parsed);
    }
  }

  return models;
}


// ------------ tHE ABOVE RETURNS ------------
[
  {
    "name": "auto",
    "description": "Auto",
    "current": true,
    "default": false
  },
  {
    "name": "composer-2-fast",
    "description": "Composer 2 Fast",
    "current": false,
    "default": true
  },
  {
    "name": "composer-2",
    "description": "Composer 2",
    "current": false,
    "default": false
  },
  ...
]

How to fetch session history

  • In the sessions table, there is a jsonl_path column. Go to directly that and parse the JSONLs from there. For gemini, the jsonl_path actually points to a gemini JSON file (since Gemini stores information in JSON rather than JSONL). DON'T use the LEGACY fetcher.

How to search conversations for each provider

  • Go to all the JSONL path directories from the database and use @vscode/ripgrep library for searching something.

How to change thinking modes for each model

Claude

  • Passed through query options through effort: <'low' | 'medium' | 'high' | 'max'>

Default is high.

Codex

  • passed through threadOptions

type ModelReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh";

type ThreadOptions = {
    model?: string;
    sandboxMode?: SandboxMode;
    workingDirectory?: string;
    skipGitRepoCheck?: boolean;
    modelReasoningEffort?: ModelReasoningEffort;
    networkAccessEnabled?: boolean;
    webSearchMode?: WebSearchMode;
    webSearchEnabled?: boolean;
    approvalPolicy?: ApprovalMode;
    additionalDirectories?: string[];
};


  • minimal is supported only by GPT-5

Gemini

  • Not changeable. We can only select the different providers that have different thinking levels by themselves.

Cursor

  • Same as gemini.

How to set/change models at start/after a session response respectively?

Claude

  • Initially can be set at start using queryOptions.model
  • Just resume the session by updating the model in threadoptions

Codex

  • Same as claude

Gemini

  • Just add the --model <model-name> property in the new spawned command. If there is something to resume, add --resume <sessionID>

Cursor

  • Just add the --model <model-name> property in the new spawned command. If there is something to resume, add --resume <sessionID>. In other words, same as gemini.