File size: 4,657 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
// API-key model-permission matching: Claude-Code alias/prefix resolution + wildcard/glob pattern
// matching used to decide whether a model is permitted for a key. Pure logic (no DB) extracted from
// db/apiKeys.ts (god-file decomposition); behavior is byte-identical to the original inline defs.

export const CLAUDE_CODE_PROVIDER_PREFIXES = new Set(["cc", "claude"]);

export const CLAUDE_CODE_SHORT_ALIASES = new Set(["sonnet", "opus", "haiku", "fable"]);

export function isTruthyEnvFlag(value: string | undefined): boolean {
  return typeof value === "string" && /^(1|true|yes|on)$/i.test(value.trim());
}

export async function preferClaudeCodeForUnprefixedClaudeModels(): Promise<boolean> {
  try {
    const { getCachedSettings } = await import("../readCache");
    const settings = await getCachedSettings();
    if (typeof settings.preferClaudeCodeForUnprefixedClaudeModels === "boolean") {
      return settings.preferClaudeCodeForUnprefixedClaudeModels;
    }
  } catch {
    // Standalone DB usage may not have the settings cache ready.
  }
  return isTruthyEnvFlag(process.env.OMNIROUTE_PREFER_CLAUDE_CODE_FOR_UNPREFIXED_CLAUDE_MODELS);
}

export function stripExtendedContextSuffix(modelId: string): string {
  return modelId.endsWith("[1m]") ? modelId.slice(0, -4) : modelId;
}

export function isPotentialUnprefixedClaudeCodeModel(modelId: string): boolean {
  const clean = stripExtendedContextSuffix(modelId.trim());
  return /^claude-/i.test(clean) || CLAUDE_CODE_SHORT_ALIASES.has(clean.toLowerCase());
}

export function addModelCandidate(candidates: Set<string>, modelId: string): void {
  const clean = modelId.trim();
  if (!clean) return;
  candidates.add(clean);
  candidates.add(stripExtendedContextSuffix(clean));
}

export function modelPatternMatches(pattern: string, candidates: string[]): boolean {
  for (const candidate of candidates) {
    if (pattern === candidate) return true;
    if (pattern.endsWith("/*")) {
      const prefix = pattern.slice(0, -2);
      if (candidate.startsWith(prefix + "/") || candidate.startsWith(prefix)) {
        return true;
      }
    }
    if (pattern.includes("*") && matchesWildcardPattern(pattern, candidate)) {
      return true;
    }
  }
  return false;
}

export function hasClaudeCodeWildcardPermission(
  allowedModels: string[] | undefined,
  candidates: string[]
): boolean {
  if (!allowedModels || allowedModels.length === 0) return false;
  return allowedModels.some(
    (pattern) =>
      (pattern === "cc/*" || pattern === "claude/*") &&
      candidates.some((candidate) => modelPatternMatches(pattern, [candidate]))
  );
}

/**
 * Match an API-key wildcard scope pattern against a model id without
 * compiling a RegExp from string concatenation (avoid ReDoS exposure on
 * operator-supplied patterns and silence the Semgrep `js/regex-injection`
 * advisory for `new RegExp(<dynamic>)`).
 *
 * Supported pattern syntax (only what real scopes use):
 *   - literal segments
 *   - `*` matches any run of characters, but does NOT cross `/`
 *
 * Walks the pattern token-by-token: each `*` consumes the longest possible
 * run within the current path segment, then the next literal anchor must
 * appear before the segment boundary. Worst-case complexity is O(n*m)
 * where n = pattern length, m = candidate length — there is no nested
 * backtracking that could explode adversarially.
 */
export function matchesWildcardPattern(pattern: string, candidate: string): boolean {
  const pSegs = pattern.split("/");
  const cSegs = candidate.split("/");
  if (pSegs.length !== cSegs.length) return false;
  for (let i = 0; i < pSegs.length; i++) {
    if (!segmentMatchesWildcard(pSegs[i], cSegs[i])) return false;
  }
  return true;
}

export function segmentMatchesWildcard(pattern: string, segment: string): boolean {
  if (pattern === segment) return true;
  if (!pattern.includes("*")) return false;
  const parts = pattern.split("*");
  // Anchor first literal to the start.
  let cursor = 0;
  const first = parts[0];
  if (first) {
    if (!segment.startsWith(first)) return false;
    cursor = first.length;
  }
  // Anchor last literal to the end.
  const last = parts[parts.length - 1];
  const endLimit = segment.length - last.length;
  if (last) {
    if (!segment.endsWith(last)) return false;
  }
  // Each middle literal must appear in order between cursor and endLimit.
  for (let i = 1; i < parts.length - 1; i++) {
    const piece = parts[i];
    if (!piece) continue;
    const idx = segment.indexOf(piece, cursor);
    if (idx === -1 || idx + piece.length > endLimit) return false;
    cursor = idx + piece.length;
  }
  return cursor <= endLimit;
}