Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,77 @@ jobs:
generate_release_notes: false
draft: true
prerelease: true

auto-release:
name: Push(main) / Auto Release
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: test-merge
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive

- name: Check if already tagged
id: check
run: |
# Skip if this commit already has a stable version tag
for tag in $(git tag --points-at HEAD 2>/dev/null); do
if echo "$tag" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "Commit already tagged as $tag, skipping."
exit 0
fi
done
echo "skip=false" >> "$GITHUB_OUTPUT"

- name: Setup Bun
if: steps.check.outputs.skip != 'true'
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Install dependencies
if: steps.check.outputs.skip != 'true'
run: bun install

- name: Compute release metadata
if: steps.check.outputs.skip != 'true'
id: meta
run: |
bun run scripts/release-meta.ts --allow-invalid --github-output "$GITHUB_OUTPUT"

# Squash merges lose individual commit types, so if bump is
# "none" but there are unreleased commits, default to patch.
BUMP=$(grep '^bump=' "$GITHUB_OUTPUT" | cut -d= -f2)
COUNT=$(grep '^commit_count=' "$GITHUB_OUTPUT" | cut -d= -f2)
if [ "$BUMP" = "none" ] && [ "$COUNT" -gt 0 ]; then
echo "Bump was 'none' with $COUNT commits — overriding to 'patch'"
LATEST=$(git tag --list 'v*.*.*' --sort=-version:refname \
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1)
if [ -n "$LATEST" ]; then
IFS='.' read -r MAJ MIN PAT <<< "${LATEST#v}"
NEXT="v${MAJ}.${MIN}.$((PAT + 1))"
else
NEXT="v0.1.0"
fi
echo "next_version=${NEXT}" >> "$GITHUB_OUTPUT"
echo "bump=patch" >> "$GITHUB_OUTPUT"
fi

- name: Create release
if: steps.check.outputs.skip != 'true' && steps.meta.outputs.bump != 'none'
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.meta.outputs.next_version }}
target_commitish: ${{ github.sha }}
name: ${{ steps.meta.outputs.next_version }}
body: ${{ steps.meta.outputs.release_notes }}
generate_release_notes: false
draft: false
prerelease: false
43 changes: 40 additions & 3 deletions src/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,31 @@ export function insertError(
).run(messageId, sessionId, errorType, message, createdAt);
}

// Per-million-token pricing by model family
const MODEL_PRICING: Record<string, { input: number; output: number; cacheRead: number }> = {
"claude-opus-4": { input: 15.0, output: 75.0, cacheRead: 1.5 },
"claude-sonnet-4": { input: 3.0, output: 15.0, cacheRead: 0.3 },
"claude-haiku-4": { input: 0.8, output: 4.0, cacheRead: 0.08 },
};
const DEFAULT_PRICING = { input: 3.0, output: 15.0, cacheRead: 0.3 };

export function estimateCost(
model: string,
inputTokens: number,
outputTokens: number,
cacheTokens: number
): number {
// Match model family: "claude-sonnet-4-20250514" → "claude-sonnet-4"
const family = Object.keys(MODEL_PRICING).find((k) => model.startsWith(k));
const pricing = family ? MODEL_PRICING[family] : DEFAULT_PRICING;
return (
(inputTokens * pricing.input +
outputTokens * pricing.output +
cacheTokens * pricing.cacheRead) /
1_000_000
);
}

export function upsertSessionCosts(
db: Database,
sessionId: string,
Expand All @@ -728,16 +753,28 @@ export function upsertSessionCosts(
cacheTokens: number,
durationMs: number
): void {
const modelName = model || "unknown";
const cost = estimateCost(modelName, inputTokens, outputTokens, cacheTokens);
db.prepare(
`INSERT INTO smriti_session_costs(session_id, model, total_input_tokens, total_output_tokens, total_cache_tokens, turn_count, total_duration_ms)
VALUES(?, ?, ?, ?, ?, 1, ?)
`INSERT INTO smriti_session_costs(session_id, model, total_input_tokens, total_output_tokens, total_cache_tokens, estimated_cost_usd, turn_count, total_duration_ms)
VALUES(?, ?, ?, ?, ?, ?, 1, ?)
ON CONFLICT(session_id, model) DO UPDATE SET
total_input_tokens = total_input_tokens + excluded.total_input_tokens,
total_output_tokens = total_output_tokens + excluded.total_output_tokens,
total_cache_tokens = total_cache_tokens + excluded.total_cache_tokens,
estimated_cost_usd = estimated_cost_usd + excluded.estimated_cost_usd,
turn_count = turn_count + 1,
total_duration_ms = total_duration_ms + excluded.total_duration_ms`
).run(sessionId, model || "unknown", inputTokens, outputTokens, cacheTokens, durationMs);
).run(sessionId, modelName, inputTokens, outputTokens, cacheTokens, cost, durationMs);
}

export function deleteSidecarRows(db: Database, sessionId: string): void {
db.prepare(`DELETE FROM smriti_tool_usage WHERE session_id = ?`).run(sessionId);
db.prepare(`DELETE FROM smriti_file_operations WHERE session_id = ?`).run(sessionId);
db.prepare(`DELETE FROM smriti_commands WHERE session_id = ?`).run(sessionId);
db.prepare(`DELETE FROM smriti_errors WHERE session_id = ?`).run(sessionId);
db.prepare(`DELETE FROM smriti_git_operations WHERE session_id = ?`).run(sessionId);
db.prepare(`DELETE FROM smriti_session_costs WHERE session_id = ?`).run(sessionId);
}

export function insertGitOperation(
Expand Down