feat(gemini): Skiva 1 – Gemini 2.5 Flash som lärar-tier för kylskåpsskanning

- Gemini-adapter bakom AamosClient-interface (AAMOS_MODE=gemini)
- Serversida/worker: hämtar bild, anropar Gemini, mappar mot canonical_ingredients
- Kostnad/tokens bokförs i ai_usage_counters; global dagsbudget via BudgetStore
- Redis-backed budget i worker, in-memory i tester
- Migration 0019: ai_cost_usd_microcents
- Hermetiska tester med inspelad fixture; separat pnpm eval:scan
- docs/09 uppdaterad ärligt: AAMOS-status, Gemini-flöde, säkerhet/kostnad
- REQUIRE_REAL=1 stödjer AAMOS_MODE=gemini; deploy-grind uppdaterad
This commit is contained in:
Sven (AAMOS AI)
2026-08-08 03:23:44 +07:00
parent 7894a1ec06
commit 050c958285
18 changed files with 1055 additions and 86 deletions
+38 -2
View File
@@ -8,6 +8,7 @@ import {
type TaskOutput,
} from "./tasks.js";
import { mockOutputFor } from "./mock.js";
import { GeminiAamosClient, type BudgetStore } from "./gemini.js";
import type { LocaleContext } from "@app/shared-types";
@@ -31,6 +32,8 @@ export interface AamosResult<T extends AamosTaskType> {
promptVersion?: string | undefined;
latencyMs?: number | undefined;
costUsd?: number | undefined;
inputTokens?: number | undefined;
outputTokens?: number | undefined;
}
/**
@@ -248,12 +251,44 @@ export interface AamosEnv {
AAMOS_API_URL?: string | undefined;
AAMOS_API_KEY?: string | undefined;
AAMOS_TIMEOUT_MS?: string | undefined;
GEMINI_API_KEY?: string | undefined;
GEMINI_MODEL?: string | undefined;
GEMINI_TIMEOUT_MS?: string | undefined;
GEMINI_DAILY_BUDGET_USD?: string | undefined;
}
/** Fabrik: http mot riktiga AAMOS (default), mock för dev/test. */
export function createAamosClient(env: AamosEnv = process.env): AamosClient {
export interface CreateAamosClientOptions {
budgetStore?: BudgetStore;
fetchImpl?: typeof fetch;
}
/** Fabrik: http mot riktiga AAMOS, gemini-lärar-tier, eller mock för dev/test. */
export function createAamosClient(
env: AamosEnv = process.env,
options: CreateAamosClientOptions = {},
): AamosClient {
const mode = env.AAMOS_MODE ?? "http";
if (mode === "mock") return new MockAamosClient();
if (mode === "gemini") {
const apiKey = env.GEMINI_API_KEY;
if (!apiKey) {
throw new Error(
"AAMOS_MODE=gemini kräver GEMINI_API_KEY. " +
"Sätt AAMOS_MODE=mock för lokal utveckling utan Gemini-åtkomst.",
);
}
return new GeminiAamosClient({
apiKey,
model: env.GEMINI_MODEL,
timeoutMs: env.GEMINI_TIMEOUT_MS ? Number(env.GEMINI_TIMEOUT_MS) : undefined,
dailyBudgetUsd: env.GEMINI_DAILY_BUDGET_USD ? Number(env.GEMINI_DAILY_BUDGET_USD) : undefined,
budgetStore: options.budgetStore,
fetchImpl: options.fetchImpl,
});
}
const baseUrl = env.AAMOS_API_URL;
const apiKey = env.AAMOS_API_KEY;
if (!baseUrl || !apiKey) {
@@ -266,6 +301,7 @@ export function createAamosClient(env: AamosEnv = process.env): AamosClient {
baseUrl,
apiKey,
timeoutMs: env.AAMOS_TIMEOUT_MS ? Number(env.AAMOS_TIMEOUT_MS) : undefined,
fetchImpl: options.fetchImpl,
});
}