sentinelayer-cli 0.37.1 → 0.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentinelayer-cli",
3
- "version": "0.37.1",
3
+ "version": "0.38.0",
4
4
  "description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -48,6 +48,7 @@
48
48
  "url": "https://github.com/mrrCarter/create-sentinelayer/issues"
49
49
  },
50
50
  "dependencies": {
51
+ "@anthropic-ai/tokenizer": "0.0.4",
51
52
  "@axe-core/playwright": "4.11.2",
52
53
  "@babel/parser": "7.29.2",
53
54
  "axe-core": "4.11.3",
@@ -60,6 +61,7 @@
60
61
  "picocolors": "1.1.1",
61
62
  "playwright": "1.59.1",
62
63
  "prompts": "2.4.2",
64
+ "tiktoken": "1.0.22",
63
65
  "yaml": "2.8.3",
64
66
  "zod": "4.1.12"
65
67
  },
@@ -1,21 +1,24 @@
1
+ import { countTokens as countAnthropicTokens } from "@anthropic-ai/tokenizer";
2
+ import { encoding_for_model, get_encoding } from "tiktoken";
3
+
1
4
  // Provider-aware token estimator (#A12, spec §5.2).
2
5
  //
3
6
  // The rest of the CLI has been guessing token counts with `text.length / 4`
4
7
  // since v0.1. That's off by 20-40% vs. the real tokenizer on prose, and
5
8
  // wildly off on code (identifiers are much more tokens per char than prose).
6
- // This module ships a zero-dep heuristic that is significantly more accurate
7
- // and — critically — provider-aware so budget calculations stop rewarding
8
- // whoever has the larger BPE vocabulary.
9
+ // This module uses the real Anthropic and OpenAI tokenizers where available
10
+ // and keeps the calibrated provider-aware heuristic as the fail-safe for
11
+ // unsupported providers or tokenizer runtime failures.
9
12
  //
10
13
  // Design goals:
11
- // - Zero runtime dependencies. @anthropic-ai/tokenizer and tiktoken are
12
- // multi-MB WASM payloads we're not willing to add at CLI-install time.
13
- // - API stable enough that swapping in the real tokenizer later is a
14
- // strict drop-in — pass `{ backend: fn }` to `estimateTokens` and the
15
- // backend takes precedence over the heuristic.
14
+ // - Real tokenizer first for supported providers.
15
+ // - API stable enough for tests and callers that need custom counting:
16
+ // pass `{ backend: fn }` to `estimateTokens` and the backend takes
17
+ // precedence over the built-in tokenizers.
16
18
  // - Calibrated ratios per provider family. Numbers below are measured
17
19
  // against published BPE stats for cl100k_base (OpenAI), claude (Anthropic),
18
- // and gemini (Google) across a mix of English prose + JS/TS source.
20
+ // and gemini (Google) across a mix of English prose + JS/TS source, and
21
+ // are only used when a real tokenizer cannot be applied.
19
22
 
20
23
  const PROVIDER_FAMILIES = Object.freeze(["anthropic", "openai", "google", "unknown"]);
21
24
 
@@ -51,6 +54,8 @@ const MODEL_PROVIDER_RULES = [
51
54
  { pattern: /^google[/:]/i, family: "google" },
52
55
  ];
53
56
 
57
+ const OPENAI_DEFAULT_ENCODING = "cl100k_base";
58
+
54
59
  // Detect provider family from a loose model id: Anthropic conventions like
55
60
  // "claude-opus-4-7", OpenAI "gpt-5.3-codex" / "o4-mini" / "codex-mini-2026",
56
61
  // Google "gemini-2.5-pro". Unknown ids fall back to the generic tokenizer.
@@ -85,50 +90,111 @@ function countWords(text) {
85
90
  return parts.length;
86
91
  }
87
92
 
93
+ function safePositiveCeil(value) {
94
+ const normalized = Number(value);
95
+ if (!Number.isFinite(normalized) || normalized < 0) {
96
+ return null;
97
+ }
98
+ return Math.max(1, Math.ceil(normalized));
99
+ }
100
+
101
+ function heuristicEstimateTokens(str, family) {
102
+ const charsPerToken = CHARS_PER_TOKEN[family] || CHARS_PER_TOKEN.unknown;
103
+ const tokensPerWord = TOKENS_PER_WORD[family] || TOKENS_PER_WORD.unknown;
104
+
105
+ const normalized = str.replace(/\s+/g, " ").trim();
106
+ if (!normalized) {
107
+ return 0;
108
+ }
109
+ const charEstimate = Math.ceil(normalized.length / charsPerToken);
110
+ const wordEstimate = Math.ceil(countWords(normalized) * tokensPerWord);
111
+ // Blend: the higher-accuracy answer depends on whether the input is
112
+ // whitespace-sparse (code/json/base64 — char estimate wins) or
113
+ // whitespace-dense prose (word estimate is more accurate). Take the max
114
+ // of the two, because underestimating token counts blows budgets; this
115
+ // biases cost estimates slightly on the safe side.
116
+ return Math.max(1, charEstimate, wordEstimate);
117
+ }
118
+
119
+ function countOpenAiTokens(str, model) {
120
+ let encoder = null;
121
+ try {
122
+ encoder = model
123
+ ? encoding_for_model(String(model).trim())
124
+ : get_encoding(OPENAI_DEFAULT_ENCODING);
125
+ } catch {
126
+ encoder = get_encoding(OPENAI_DEFAULT_ENCODING);
127
+ }
128
+
129
+ try {
130
+ return encoder.encode(str).length;
131
+ } finally {
132
+ if (encoder && typeof encoder.free === "function") {
133
+ encoder.free();
134
+ }
135
+ }
136
+ }
137
+
138
+ export function countWithProviderTokenizer(
139
+ text,
140
+ { provider = "", model = "" } = {}
141
+ ) {
142
+ const str = typeof text === "string" ? text : text == null ? "" : String(text);
143
+ if (!str || !str.trim()) {
144
+ return 0;
145
+ }
146
+
147
+ let family = normalizeProviderFamily(provider);
148
+ if (family === "unknown" && model) {
149
+ family = detectProviderFamily(model);
150
+ }
151
+
152
+ if (family === "anthropic") {
153
+ return safePositiveCeil(countAnthropicTokens(str));
154
+ }
155
+ if (family === "openai") {
156
+ return safePositiveCeil(countOpenAiTokens(str, model));
157
+ }
158
+ return null;
159
+ }
160
+
88
161
  // Estimate token count for a text against a provider family. Uses a blend
89
- // of char-per-token and word-per-token so short inputs (which are mostly
90
- // function of token-per-word behavior) and long runs of no-break chars
91
- // (where the char ratio dominates) both get sensible answers.
162
+ // of real provider tokenizers and the provider-aware fallback heuristic.
92
163
  //
93
164
  // Options:
94
165
  // - provider: "anthropic" | "openai" | "google" | "unknown" (explicit)
95
166
  // - model: model id, used to infer provider when provider is omitted
96
- // - backend: fn(text) -> number. Overrides the heuristic. This is the
97
- // hook for swapping in @anthropic-ai/tokenizer / tiktoken
98
- // without rewriting callers.
167
+ // - backend: fn(text) -> number. Overrides the built-in tokenizer path.
99
168
  export function estimateTokens(
100
169
  text,
101
170
  { provider = "", model = "", backend = null } = {}
102
171
  ) {
103
172
  const str = typeof text === "string" ? text : text == null ? "" : String(text);
104
- if (!str) {
173
+ if (!str || !str.trim()) {
105
174
  return 0;
106
175
  }
107
176
  if (typeof backend === "function") {
108
- const custom = Number(backend(str));
109
- if (Number.isFinite(custom) && custom >= 0) {
110
- return Math.max(1, Math.ceil(custom));
177
+ const custom = safePositiveCeil(backend(str));
178
+ if (custom !== null) {
179
+ return custom;
111
180
  }
112
181
  }
182
+
113
183
  let family = normalizeProviderFamily(provider);
114
184
  if (family === "unknown" && model) {
115
185
  family = detectProviderFamily(model);
116
186
  }
117
- const charsPerToken = CHARS_PER_TOKEN[family] || CHARS_PER_TOKEN.unknown;
118
- const tokensPerWord = TOKENS_PER_WORD[family] || TOKENS_PER_WORD.unknown;
119
187
 
120
- const normalized = str.replace(/\s+/g, " ").trim();
121
- if (!normalized) {
122
- return 0;
188
+ try {
189
+ const tokenizerCount = countWithProviderTokenizer(str, { provider: family, model });
190
+ if (tokenizerCount !== null) {
191
+ return tokenizerCount;
192
+ }
193
+ } catch {
194
+ // Keep budget calculations available even if a native/WASM tokenizer
195
+ // cannot initialize in a constrained runtime.
123
196
  }
124
- const charEstimate = Math.ceil(normalized.length / charsPerToken);
125
- const wordEstimate = Math.ceil(countWords(normalized) * tokensPerWord);
126
- // Blend: the higher-accuracy answer depends on whether the input is
127
- // whitespace-sparse (code/json/base64 — char estimate wins) or
128
- // whitespace-dense prose (word estimate is more accurate). Take the max
129
- // of the two, because underestimating token counts blows budgets; this
130
- // biases cost estimates slightly on the safe side.
131
- return Math.max(1, charEstimate, wordEstimate);
197
+ return heuristicEstimateTokens(str, family);
132
198
  }
133
199
 
134
200
  // Combined token count + cost calculation for a single request. Consumers
@@ -157,4 +223,9 @@ export function estimateTokensForMessages(
157
223
  return total;
158
224
  }
159
225
 
160
- export { CHARS_PER_TOKEN, PROVIDER_FAMILIES, TOKENS_PER_WORD };
226
+ export {
227
+ CHARS_PER_TOKEN,
228
+ OPENAI_DEFAULT_ENCODING,
229
+ PROVIDER_FAMILIES,
230
+ TOKENS_PER_WORD,
231
+ };
package/src/legacy-cli.js CHANGED
@@ -2517,7 +2517,7 @@ jobs:
2517
2517
  fi
2518
2518
  - name: Run Omar Gate
2519
2519
  id: omar
2520
- uses: mrrCarter/sentinelayer-v1-action@5c4d8c175eb117ea5256452e50e01249ab126998
2520
+ uses: mrrCarter/sentinelayer-v1-action@a496be33a466c0cc3f8616d66bbd7d78f7d3c31d
2521
2521
  with:
2522
2522
  github_token: \${{ github.token }}
2523
2523
  openai_api_key: \${{ secrets.OPENAI_API_KEY }}
@@ -2527,9 +2527,9 @@ jobs:
2527
2527
  sentinelayer_managed_llm: \${{ secrets.${normalizedSecret} != '' }}
2528
2528
  scan_mode: \${{ github.event_name == 'workflow_dispatch' && inputs.scan_mode || 'deep' }}
2529
2529
  severity_gate: \${{ github.event_name == 'workflow_dispatch' && inputs.severity_gate || 'P1' }}
2530
- model: \${{ secrets.OPENAI_API_KEY != '' && 'gpt-5.3-codex' || ((secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-3.1-flash-lite' || 'gpt-5.3-codex') }}
2530
+ model: \${{ secrets.OPENAI_API_KEY != '' && 'gpt-5.3-codex' || ((secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-2.5-flash' || 'gpt-5.3-codex') }}
2531
2531
  codex_model: gpt-5.3-codex
2532
- model_fallback: \${{ (secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-3.1-flash-lite' || 'gpt-4.1-mini' }}
2532
+ model_fallback: \${{ (secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-2.5-flash' || 'gpt-4.1-mini' }}
2533
2533
  use_codex: \${{ secrets.OPENAI_API_KEY != '' || (secrets.GOOGLE_GEMINI_API_KEY == '' && secrets.GOOGLE_API_KEY == '') }}
2534
2534
  codex_only: "false"
2535
2535
  llm_failure_policy: block
@@ -2585,8 +2585,8 @@ jobs:
2585
2585
  OMAR_GOOGLE_KEY_PRESENT: \${{ secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '' }}
2586
2586
  OMAR_MANAGED_LLM: \${{ secrets.${normalizedSecret} != '' }}
2587
2587
  OMAR_LLM_PROVIDER: \${{ secrets.OPENAI_API_KEY != '' && 'openai' || ((secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'google' || 'openai') }}
2588
- OMAR_MODEL: \${{ secrets.OPENAI_API_KEY != '' && 'gpt-5.3-codex' || ((secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-3.1-flash-lite' || 'gpt-5.3-codex') }}
2589
- OMAR_MODEL_FALLBACK: \${{ (secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-3.1-flash-lite' || 'gpt-4.1-mini' }}
2588
+ OMAR_MODEL: \${{ secrets.OPENAI_API_KEY != '' && 'gpt-5.3-codex' || ((secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-2.5-flash' || 'gpt-5.3-codex') }}
2589
+ OMAR_MODEL_FALLBACK: \${{ (secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-2.5-flash' || 'gpt-4.1-mini' }}
2590
2590
  run: |
2591
2591
  set -euo pipefail
2592
2592
  mkdir -p omar-artifacts
@@ -2626,7 +2626,7 @@ jobs:
2626
2626
  },
2627
2627
  "scan": {
2628
2628
  "mode": env("OMAR_SCAN_MODE", "deep"),
2629
- "action_ref": "mrrCarter/sentinelayer-v1-action@5c4d8c175eb117ea5256452e50e01249ab126998",
2629
+ "action_ref": "mrrCarter/sentinelayer-v1-action@a496be33a466c0cc3f8616d66bbd7d78f7d3c31d",
2630
2630
  "llm_provider": env("OMAR_LLM_PROVIDER", "openai"),
2631
2631
  "model": env("OMAR_MODEL", "gpt-5.3-codex"),
2632
2632
  "model_fallback": env("OMAR_MODEL_FALLBACK", "gpt-4.1-mini"),
@@ -376,6 +376,52 @@ function summarizeFindings(findings = []) {
376
376
  };
377
377
  }
378
378
 
379
+ function buildPersonaHealthFindings(personaHealth = {}) {
380
+ if (personaHealth.healthy || personaHealth.total <= 0) {
381
+ return [];
382
+ }
383
+
384
+ const warnings = Array.isArray(personaHealth.warnings) ? personaHealth.warnings : [];
385
+ const errorCount = Number(personaHealth.error || 0);
386
+ const total = Number(personaHealth.total || 0);
387
+ const errorRatio = Number(personaHealth.errorRatio || 0);
388
+ const totalCostUsd = Number(personaHealth.totalCostUsd || 0);
389
+ const allPersonasFailed = total > 0 && errorCount === total;
390
+ const severity = allPersonasFailed ? "P0" : "P1";
391
+ let message = `Omar AI coverage degraded: ${errorCount}/${total} dispatched personas errored (${Math.round(errorRatio * 100)}%).`;
392
+ if (allPersonasFailed) {
393
+ message = `Omar AI coverage failed closed: all ${total} dispatched personas errored before producing findings.`;
394
+ } else if (errorCount === 0 && totalCostUsd <= 0) {
395
+ message = "Omar AI coverage failed closed: personas reported ok but total AI cost was $0.00, indicating no LLM calls were made.";
396
+ }
397
+
398
+ return [
399
+ {
400
+ severity,
401
+ file: "<omargate>",
402
+ line: 1,
403
+ message,
404
+ category: "Omar AI Coverage",
405
+ source: "orchestrator",
406
+ layer: "orchestrator",
407
+ confidence: 1,
408
+ confidenceFloor: 0,
409
+ sources: ["orchestrator"],
410
+ remediation:
411
+ "Fix SentinelLayer managed LLM proxy/provider credentials or run with --no-ai when deterministic-only coverage is intentional.",
412
+ evidence: warnings,
413
+ personaHealth: {
414
+ ok: Number(personaHealth.ok || 0),
415
+ error: errorCount,
416
+ skipped: Number(personaHealth.skipped || 0),
417
+ total,
418
+ errorRatio,
419
+ totalCostUsd,
420
+ },
421
+ },
422
+ ];
423
+ }
424
+
379
425
  function personaAgentFromIdentity(identity = {}) {
380
426
  return {
381
427
  id: identity.id,
@@ -443,6 +489,7 @@ async function runOmarPersonaSwarm({
443
489
  usageSessionId = "",
444
490
  requireUsageLedger = false,
445
491
  usageRecorder = undefined,
492
+ aiReviewRunner = runAiReviewLayer,
446
493
  } = {}) {
447
494
  const scope = buildPersonaFileScope({ deterministic });
448
495
  const decision = decideSwarm({ scope });
@@ -518,7 +565,7 @@ async function runOmarPersonaSwarm({
518
565
  }
519
566
 
520
567
  try {
521
- const result = await runAiReviewLayer({
568
+ const result = await aiReviewRunner({
522
569
  targetPath,
523
570
  mode: "full",
524
571
  runId: subagentRunId,
@@ -827,6 +874,7 @@ export async function runOmarGateOrchestrator({
827
874
  requireUsageLedger = false,
828
875
  usageSessionId = "",
829
876
  usageRecorder = undefined,
877
+ aiReviewRunner = runAiReviewLayer,
830
878
  } = {}) {
831
879
  const runId = `omargate-${Date.now()}-${randomUUID().slice(0, 8)}`;
832
880
  const startTime = Date.now();
@@ -999,6 +1047,7 @@ export async function runOmarGateOrchestrator({
999
1047
  requireUsageLedger,
1000
1048
  usageSessionId,
1001
1049
  usageRecorder,
1050
+ aiReviewRunner,
1002
1051
  });
1003
1052
 
1004
1053
  if (swarmResult) {
@@ -1048,7 +1097,7 @@ export async function runOmarGateOrchestrator({
1048
1097
  deterministicSummary: personaDeterministic?.summary || detSummary,
1049
1098
  });
1050
1099
 
1051
- const result = await runAiReviewLayer({
1100
+ const result = await aiReviewRunner({
1052
1101
  targetPath,
1053
1102
  mode: "full",
1054
1103
  runId: `${runId}-${personaId}`,
@@ -1210,8 +1259,8 @@ export async function runOmarGateOrchestrator({
1210
1259
  defaultConfidenceFloor: OMARGATE_DEFAULT_CONFIDENCE_FLOOR,
1211
1260
  confidenceFloors: OMARGATE_CONFIDENCE_FLOORS,
1212
1261
  });
1213
- const reconciledFindings = reconciled.findings;
1214
- const reconciledSummary = reconciled.summary;
1262
+ let reconciledFindings = reconciled.findings;
1263
+ let reconciledSummary = reconciled.summary;
1215
1264
  const droppedBelowConfidence = Number(reconciledSummary?.droppedBelowConfidence || 0);
1216
1265
  const candidateFindingCount = detFindings.length + allAiFindings.length;
1217
1266
  const dedupedCount = Math.max(
@@ -1241,6 +1290,7 @@ export async function runOmarGateOrchestrator({
1241
1290
  skipped: personaSkippedCount,
1242
1291
  total: totalPersonas,
1243
1292
  errorRatio,
1293
+ totalCostUsd: totalCost,
1244
1294
  healthy: aiCoverageHealthy || dryRun,
1245
1295
  warnings: [],
1246
1296
  };
@@ -1261,6 +1311,12 @@ export async function runOmarGateOrchestrator({
1261
1311
  }
1262
1312
  }
1263
1313
 
1314
+ const personaHealthFindings = buildPersonaHealthFindings(personaHealth);
1315
+ if (personaHealthFindings.length > 0) {
1316
+ reconciledFindings = [...reconciledFindings, ...personaHealthFindings];
1317
+ reconciledSummary = summarizeFindings(reconciledFindings);
1318
+ }
1319
+
1264
1320
  const result = {
1265
1321
  runId,
1266
1322
  mode,
@@ -1286,6 +1342,7 @@ export async function runOmarGateOrchestrator({
1286
1342
  findingsBySource: {
1287
1343
  deterministic: detFindings.length,
1288
1344
  ai: allAiFindings.length,
1345
+ orchestrator: personaHealthFindings.length,
1289
1346
  reconciled: reconciledFindings.length,
1290
1347
  droppedBelowConfidence,
1291
1348
  },
@@ -1295,6 +1352,7 @@ export async function runOmarGateOrchestrator({
1295
1352
  reconciliation: {
1296
1353
  deterministicFindings: detFindings.length,
1297
1354
  aiFindings: allAiFindings.length,
1355
+ orchestratorFindings: personaHealthFindings.length,
1298
1356
  reconciledFindings: reconciledFindings.length,
1299
1357
  dedupedCount,
1300
1358
  droppedBelowConfidence,
@@ -2,7 +2,7 @@ import YAML from "yaml";
2
2
 
3
3
  export const DEFAULT_SCAN_WORKFLOW_PATH = ".github/workflows/omar-gate.yml";
4
4
  export const DEFAULT_SCAN_SECRET_NAME = "SENTINELAYER_TOKEN";
5
- export const SENTINELAYER_ACTION_REF = "mrrCarter/sentinelayer-v1-action@5c4d8c175eb117ea5256452e50e01249ab126998";
5
+ export const SENTINELAYER_ACTION_REF = "mrrCarter/sentinelayer-v1-action@a496be33a466c0cc3f8616d66bbd7d78f7d3c31d";
6
6
  export const SUPPORTED_E2E_HINTS = Object.freeze(["auto", "yes", "no"]);
7
7
  export const SUPPORTED_PLAYWRIGHT_MODES = Object.freeze(["auto", "off", "baseline", "audit"]);
8
8
 
@@ -236,10 +236,10 @@ export function buildSecurityReviewWorkflow({ secretName = DEFAULT_SCAN_SECRET_N
236
236
  scan_mode: profile.scanMode || "deep",
237
237
  severity_gate: profile.severityGate || "P1",
238
238
  model:
239
- "${{ secrets.OPENAI_API_KEY != '' && 'gpt-5.3-codex' || ((secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-3.1-flash-lite' || 'gpt-5.3-codex') }}",
239
+ "${{ secrets.OPENAI_API_KEY != '' && 'gpt-5.3-codex' || ((secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-2.5-flash' || 'gpt-5.3-codex') }}",
240
240
  codex_model: "gpt-5.3-codex",
241
241
  model_fallback:
242
- "${{ (secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-3.1-flash-lite' || 'gpt-4.1-mini' }}",
242
+ "${{ (secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-2.5-flash' || 'gpt-4.1-mini' }}",
243
243
  use_codex:
244
244
  "${{ secrets.OPENAI_API_KEY != '' || (secrets.GOOGLE_GEMINI_API_KEY == '' && secrets.GOOGLE_API_KEY == '') }}",
245
245
  codex_only: "false",
@@ -306,9 +306,9 @@ export function buildSecurityReviewWorkflow({ secretName = DEFAULT_SCAN_SECRET_N
306
306
  OMAR_LLM_PROVIDER:
307
307
  "${{ secrets.OPENAI_API_KEY != '' && 'openai' || ((secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'google' || 'openai') }}",
308
308
  OMAR_MODEL:
309
- "${{ secrets.OPENAI_API_KEY != '' && 'gpt-5.3-codex' || ((secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-3.1-flash-lite' || 'gpt-5.3-codex') }}",
309
+ "${{ secrets.OPENAI_API_KEY != '' && 'gpt-5.3-codex' || ((secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-2.5-flash' || 'gpt-5.3-codex') }}",
310
310
  OMAR_MODEL_FALLBACK:
311
- "${{ (secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-3.1-flash-lite' || 'gpt-4.1-mini' }}",
311
+ "${{ (secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-2.5-flash' || 'gpt-4.1-mini' }}",
312
312
  },
313
313
  run: [
314
314
  "set -euo pipefail",