Skip to content

Instantly share code, notes, and snippets.

@Misaka-0x447f
Created February 13, 2026 10:27
Show Gist options
  • Select an option

  • Save Misaka-0x447f/f8bd672f52ff5d59579b9615a2f64fec to your computer and use it in GitHub Desktop.

Select an option

Save Misaka-0x447f/f8bd672f52ff5d59579b9615a2f64fec to your computer and use it in GitHub Desktop.
#!/bin/bash
# Wrapper to invoke TypeScript statusline script
exec pnpx tsx "$(dirname "$0")/open-router-statusline.ts" --env-file=.env
#!/usr/bin/env npx tsx
/**
* OpenRouter cost tracking statusline for Claude Code
*
* Displays: Provider: model - $cost - cache discount: $saved
*
* Setup: Add to your ~/.claude/settings.json:
* {
* "statusLine": {
* "type": "command",
* "command": "/path/to/statusline.sh"
* }
* }
*
* Requires: ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY set to your OpenRouter API key
*/
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
interface StatuslineInput {
session_id: string;
transcript_path: string;
}
interface GenerationData {
total_cost: number;
cache_discount: number | null;
provider_name: string;
model: string;
}
interface State {
seen_ids: string[];
total_cost: number;
total_cache_discount: number;
last_provider: string;
last_model: string;
}
async function fetchGeneration(id: string, apiKey: string): Promise<GenerationData | null> {
try {
const res = await fetch(`https://openrouter.ai/api/v1/generation?id=${id}`, {
headers: {
Authorization: `Bearer ${apiKey}`,
},
});
if (!res.ok) {
return null;
}
const json = await res.json();
const data = json?.data;
if (!data || typeof data.total_cost !== 'number') {
return null;
}
return data;
} catch {
return null;
}
}
function extractGenerationIds(transcriptPath: string): string[] {
try {
const content = readFileSync(transcriptPath, 'utf-8');
const ids: string[] = [];
for (const line of content.split('\n')) {
if (!line.trim()) {
continue;
}
try {
const entry = JSON.parse(line);
const messageId = entry?.message?.id;
if (typeof messageId === 'string' && messageId.startsWith('gen-')) {
ids.push(messageId);
}
} catch {
// Skip malformed lines
}
}
return [...new Set(ids)];
} catch {
return [];
}
}
function loadState(statePath: string): State {
const defaultState: State = {
seen_ids: [],
total_cost: 0,
total_cache_discount: 0,
last_provider: '',
last_model: '',
};
if (!existsSync(statePath)) {
return defaultState;
}
try {
const content = readFileSync(statePath, 'utf-8');
if (!content.trim()) {
return defaultState;
}
const parsed = JSON.parse(content);
// Validate state shape
if (!Array.isArray(parsed.seen_ids)) {
return defaultState;
}
return {
seen_ids: parsed.seen_ids,
total_cost: typeof parsed.total_cost === 'number' ? parsed.total_cost : 0,
total_cache_discount:
typeof parsed.total_cache_discount === 'number' ? parsed.total_cache_discount : 0,
last_provider: typeof parsed.last_provider === 'string' ? parsed.last_provider : '',
last_model: typeof parsed.last_model === 'string' ? parsed.last_model : '',
};
} catch {
return defaultState;
}
}
function saveState(statePath: string, state: State): void {
writeFileSync(statePath, JSON.stringify(state, null, 2));
}
function shortModelName(model: string): string {
return model.replace(/^[^/]+\//, '').replace(/-\d{8}$/, '');
}
async function main(): Promise<void> {
const apiKey = process.env.openrouterApiKey;
let inputData = '';
for await (const chunk of process.stdin) {
inputData += chunk;
}
const input = JSON.parse(inputData);
const session_id = input?.session_id;
const transcript_path = input?.transcript_path;
if (typeof session_id !== 'string' || typeof transcript_path !== 'string') {
process.stdout.write('Invalid statusline input');
return;
}
const statePath = `/tmp/claude-openrouter-cost-${session_id}.json`;
const state = loadState(statePath);
const allIds = extractGenerationIds(transcript_path);
const seenSet = new Set(state.seen_ids);
const newIds = allIds.filter((id) => !seenSet.has(id));
let fetchSucceeded = 0;
let fetchFailed = 0;
for (const id of newIds) {
const gen = await fetchGeneration(id, apiKey);
if (!gen) {
fetchFailed++;
continue;
}
fetchSucceeded++;
state.total_cost += gen.total_cost ?? 0;
state.total_cache_discount += gen.cache_discount ?? 0;
if (gen.provider_name) {
state.last_provider = gen.provider_name;
}
if (gen.model) {
state.last_model = gen.model;
}
state.seen_ids.push(id);
}
saveState(statePath, state);
const shortModel = shortModelName(state.last_model);
let statusIndicator = '';
if (newIds.length > 0) {
const green = '\x1b[32m';
const red = '\x1b[31m';
const reset = '\x1b[0m';
const now = new Date();
const timeString = new Intl.DateTimeFormat('en-GB', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}).format(now);
if (fetchFailed === 0) {
statusIndicator = ` / ${green}${timeString}${reset}`;
} else {
statusIndicator = ` / ${red}${timeString}${reset}`;
}
}
if (state.last_provider) {
process.stdout.write(
`${state.last_provider}: ${shortModel} / $${state.total_cost.toPrecision(4)} / cached $${state.total_cache_discount.toPrecision(3)}${statusIndicator}`,
);
} else {
process.stdout.write(
`$${state.total_cost.toPrecision(4)} / cached $${state.total_cache_discount.toPrecision(3)}${statusIndicator}`,
);
}
}
main().catch((err) => {
process.stdout.write(`error: ${err.message}`);
});
#!/bin/bash
# Wrapper to invoke TypeScript statusline script
exec pnpx tsx "$(dirname "$0")/statusline.ts"
#!/usr/bin/env npx tsx
interface StatuslineInput {
cwd?: string;
workspace?: { current_dir?: string };
model?: { display_name?: string };
cost?: { total_cost_usd?: number };
context_window?: {
total_input_tokens?: number;
total_output_tokens?: number;
};
}
async function main(): Promise<void> {
let raw = '';
for await (const chunk of process.stdin) {
raw += chunk;
}
const data: StatuslineInput = JSON.parse(raw);
const cwd = data.workspace?.current_dir ?? data.cwd ?? '';
const model = data.model?.display_name ?? '';
const cost = data.cost?.total_cost_usd ?? 0;
const totalIn = data.context_window?.total_input_tokens ?? 0;
const totalOut = data.context_window?.total_output_tokens ?? 0;
const time = new Intl.DateTimeFormat('en-GB', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
}).format(new Date());
process.stdout.write(
`${cwd} | ${model} | $${cost.toFixed(4)} | ${time} | in:${new Intl.NumberFormat().format(totalIn)} out:${new Intl.NumberFormat().format(totalOut)}`,
);
}
main().catch((err) => {
process.stdout.write(`error: ${err.message}`);
});
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const PROFILES_DIR = path.join(__dirname, 'alt-profile.d');
const SETTINGS_FILE = path.join(__dirname, 'settings.json');
const STATE_FILE = path.join(__dirname, '.profile-state.json');
/**
* 读取配置文件
*/
function readSettings() {
try {
const content = fs.readFileSync(SETTINGS_FILE, 'utf-8');
return JSON.parse(content);
} catch (error) {
console.error(`读取配置文件失败: ${error.message}`);
process.exit(1);
}
}
/**
* 保存配置文件
*/
function saveSettings(settings) {
try {
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2));
} catch (error) {
console.error(`保存配置文件失败: ${error.message}`);
process.exit(1);
}
}
/**
* 读取所有 profiles
*/
function getProfiles() {
try {
if (!fs.existsSync(PROFILES_DIR)) {
console.error(`Profiles 目录不存在: ${PROFILES_DIR}`);
process.exit(1);
}
const files = fs.readdirSync(PROFILES_DIR).filter(f => f.endsWith('.json'));
if (files.length === 0) {
console.error(`Profiles 目录中没有任何 JSON 文件`);
process.exit(1);
}
const profiles = files.map(file => {
try {
const content = fs.readFileSync(path.join(PROFILES_DIR, file), 'utf-8');
return JSON.parse(content);
} catch (error) {
console.warn(`无法解析 profile 文件 ${file}: ${error.message}`);
return null;
}
}).filter(p => p !== null);
return profiles.sort((a, b) => (a.displayName || '').localeCompare(b.displayName || ''));
} catch (error) {
console.error(`读取 profiles 失败: ${error.message}`);
process.exit(1);
}
}
/**
* 读取当前状态
*/
function getState() {
try {
if (fs.existsSync(STATE_FILE)) {
const content = fs.readFileSync(STATE_FILE, 'utf-8');
return JSON.parse(content);
}
return { currentIndex: 0 };
} catch (error) {
return { currentIndex: 0 };
}
}
/**
* 保存状态
*/
function saveState(state) {
try {
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
} catch (error) {
console.warn(`无法保存状态: ${error.message}`);
}
}
/**
* 合并 profile 数据到设置
*/
function mergeProfile(settings, profile) {
const merged = { ...settings };
if (profile.data && typeof profile.data === 'object') {
// 递归合并 data 到 settings
for (const [key, value] of Object.entries(profile.data)) {
if (typeof value === 'object' && value !== null && typeof merged[key] === 'object') {
// 深度合并对象
merged[key] = { ...merged[key], ...value };
} else {
merged[key] = value;
}
}
}
return merged;
}
/**
* 主函数
*/
function main() {
const profiles = getProfiles();
const state = getState();
// 获取当前索引,确保在有效范围内
let currentIndex = state.currentIndex % profiles.length;
// 计算下一个 profile 的索引
const nextIndex = (currentIndex + 1) % profiles.length;
const nextProfile = profiles[nextIndex];
// 读取当前设置
let settings = readSettings();
// 合并下一个 profile 的数据
settings = mergeProfile(settings, nextProfile);
// 保存设置
saveSettings(settings);
// 保存状态
saveState({ currentIndex: nextIndex });
// 输出当前 displayName
console.log(nextProfile.displayName || '(未命名)');
}
// 运行
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment