mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 16:03:17 +00:00
fix(codex): parse multiline provider pins in OAuth guard
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { readCodexTopLevelModelProvider } from './codex-model-provider-config'
|
||||
|
||||
describe('readCodexTopLevelModelProvider', () => {
|
||||
it.each([
|
||||
['missing provider', 'model = "gpt-5"\n', null],
|
||||
['OpenAI provider', 'model_provider = "openai" # default\n', 'openai'],
|
||||
['literal custom provider', "model_provider = 'codex-lb'\n", 'codex-lb'],
|
||||
['quoted key', '"model_provider" = "codex-lb"\n', 'codex-lb'],
|
||||
['escaped quoted key', '"model\\u005Fprovider" = "codex-lb"\n', 'codex-lb'],
|
||||
['escaped provider', 'model_provider = "codex\\u002Dlb"\n', 'codex-lb'],
|
||||
['single-line multiline OpenAI', 'model_provider = """openai"""\n', 'openai'],
|
||||
['single-line multiline custom', "model_provider = '''codex-lb'''\n", 'codex-lb'],
|
||||
['CRLF custom provider', 'model = "gpt-5"\r\nmodel_provider = "codex-lb"\r\n', 'codex-lb'],
|
||||
[
|
||||
'continued multiline custom',
|
||||
['model_provider = """\\', ' codex-lb"""', ''].join('\n'),
|
||||
'codex-lb'
|
||||
],
|
||||
['profile provider', ['[profiles.custom]', 'model_provider = "codex-lb"', ''].join('\n'), null],
|
||||
[
|
||||
'provider-like multiline data',
|
||||
[
|
||||
'developer_instructions = """',
|
||||
'[profiles.custom]',
|
||||
'model_provider = "not-a-setting"',
|
||||
'"""',
|
||||
'model_provider = "openai"',
|
||||
''
|
||||
].join('\n'),
|
||||
'openai'
|
||||
],
|
||||
[
|
||||
'provider-like array data',
|
||||
[
|
||||
'notify = [',
|
||||
' "model_provider = \\"not-a-setting\\"",',
|
||||
']',
|
||||
'model_provider = "openai"',
|
||||
''
|
||||
].join('\n'),
|
||||
'openai'
|
||||
]
|
||||
])('reads %s', (_description, config, expected) => {
|
||||
expect(readCodexTopLevelModelProvider(config)).toBe(expected)
|
||||
})
|
||||
|
||||
it('uses the first duplicate assignment deterministically', () => {
|
||||
expect(
|
||||
readCodexTopLevelModelProvider(
|
||||
['model_provider = "openai"', 'model_provider = "codex-lb"', ''].join('\n')
|
||||
)
|
||||
).toBe('openai')
|
||||
})
|
||||
|
||||
it.each([
|
||||
['unquoted value', 'model_provider = codex-lb\n'],
|
||||
['unterminated value', 'model_provider = "codex-lb\n'],
|
||||
['assignment after a table', '[profiles.default]\nmodel_provider = "codex-lb"\n']
|
||||
])('ignores malformed or non-root %s', (_description, config) => {
|
||||
expect(readCodexTopLevelModelProvider(config)).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
getTomlTableHeader,
|
||||
isTomlStructuralLine,
|
||||
parseTomlSingleLineStringValue,
|
||||
parseTomlStringValue,
|
||||
updateTomlLineScanState
|
||||
} from './config-toml-line-scan'
|
||||
|
||||
@@ -10,17 +11,41 @@ import {
|
||||
// similarly named keys inside profiles or provider tables are not global pins.
|
||||
export function readCodexTopLevelModelProvider(config: string): string | null {
|
||||
let state = createTomlLineScanState()
|
||||
let lineOffset = 0
|
||||
for (const line of config.split('\n')) {
|
||||
if (isTomlStructuralLine(state)) {
|
||||
if (getTomlTableHeader(line)) {
|
||||
return null
|
||||
}
|
||||
const match = /^[ \t]*(?:model_provider|"model_provider"|'model_provider')[ \t]*=/.exec(line)
|
||||
if (match) {
|
||||
return parseTomlSingleLineStringValue(line, match[0].length)?.value ?? null
|
||||
const valueOffset = getModelProviderValueOffset(line)
|
||||
if (valueOffset !== null) {
|
||||
return parseTomlStringValue(config, lineOffset + valueOffset)?.value ?? null
|
||||
}
|
||||
}
|
||||
state = updateTomlLineScanState(state, line)
|
||||
lineOffset += line.length + 1
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function getModelProviderValueOffset(line: string): number | null {
|
||||
let index = 0
|
||||
while (line[index] === ' ' || line[index] === '\t') {
|
||||
index += 1
|
||||
}
|
||||
|
||||
if (line.startsWith('model_provider', index)) {
|
||||
index += 'model_provider'.length
|
||||
} else {
|
||||
const parsedKey = parseTomlSingleLineStringValue(line, index)
|
||||
if (parsedKey?.value !== 'model_provider') {
|
||||
return null
|
||||
}
|
||||
index = parsedKey.end
|
||||
}
|
||||
|
||||
while (line[index] === ' ' || line[index] === '\t') {
|
||||
index += 1
|
||||
}
|
||||
return line[index] === '=' ? index + 1 : null
|
||||
}
|
||||
|
||||
@@ -122,6 +122,9 @@ export function parseTomlSingleLineStringValue(
|
||||
let value = ''
|
||||
while (index < line.length) {
|
||||
const char = line[index]
|
||||
if (char === '\n' || char === '\r') {
|
||||
return null
|
||||
}
|
||||
if (char === quote) {
|
||||
return { value, start, end: index + 1 }
|
||||
}
|
||||
@@ -140,6 +143,58 @@ export function parseTomlSingleLineStringValue(
|
||||
return null
|
||||
}
|
||||
|
||||
// Why: serde accepts multiline TOML strings for String settings such as
|
||||
// model_provider, so safety classifiers cannot treat triple quotes as absent.
|
||||
export function parseTomlStringValue(source: string, offset: number): ParsedTomlString | null {
|
||||
let index = offset
|
||||
while (source[index] === ' ' || source[index] === '\t') {
|
||||
index += 1
|
||||
}
|
||||
|
||||
const delimiter = source.startsWith('"""', index)
|
||||
? '"""'
|
||||
: source.startsWith("'''", index)
|
||||
? "'''"
|
||||
: null
|
||||
if (!delimiter) {
|
||||
return parseTomlSingleLineStringValue(source, offset)
|
||||
}
|
||||
|
||||
const start = index
|
||||
index += delimiter.length
|
||||
index = skipInitialTomlMultilineNewline(source, index)
|
||||
let value = ''
|
||||
while (index < source.length) {
|
||||
if (source.startsWith(delimiter, index)) {
|
||||
return { value, start, end: index + delimiter.length }
|
||||
}
|
||||
|
||||
if (delimiter === '"""' && source[index] === '\\') {
|
||||
const continuationEnd = getTomlMultilineContinuationEnd(source, index)
|
||||
if (continuationEnd !== null) {
|
||||
index = continuationEnd
|
||||
continue
|
||||
}
|
||||
const escaped = parseTomlBasicStringEscape(source, index)
|
||||
if (!escaped) {
|
||||
return null
|
||||
}
|
||||
value += escaped.value
|
||||
index = escaped.nextIndex
|
||||
continue
|
||||
}
|
||||
|
||||
if (source[index] === '\r') {
|
||||
value += '\n'
|
||||
index += source[index + 1] === '\n' ? 2 : 1
|
||||
continue
|
||||
}
|
||||
value += source[index]
|
||||
index += 1
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function skipTomlBasicString(line: string, startIndex: number): number {
|
||||
let index = startIndex
|
||||
while (index < line.length) {
|
||||
@@ -161,6 +216,37 @@ function skipTomlLiteralString(line: string, startIndex: number): number {
|
||||
return endIndex === -1 ? line.length : endIndex + 1
|
||||
}
|
||||
|
||||
function skipInitialTomlMultilineNewline(source: string, index: number): number {
|
||||
if (source[index] === '\r' && source[index + 1] === '\n') {
|
||||
return index + 2
|
||||
}
|
||||
return source[index] === '\n' ? index + 1 : index
|
||||
}
|
||||
|
||||
function getTomlMultilineContinuationEnd(source: string, slashIndex: number): number | null {
|
||||
let index = slashIndex + 1
|
||||
while (source[index] === ' ' || source[index] === '\t') {
|
||||
index += 1
|
||||
}
|
||||
if (source[index] === '\r' && source[index + 1] === '\n') {
|
||||
index += 2
|
||||
} else if (source[index] === '\n') {
|
||||
index += 1
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
while (
|
||||
source[index] === ' ' ||
|
||||
source[index] === '\t' ||
|
||||
source[index] === '\n' ||
|
||||
source[index] === '\r'
|
||||
) {
|
||||
index += 1
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
function parseTomlBasicStringEscape(
|
||||
line: string,
|
||||
slashIndex: number
|
||||
|
||||
Reference in New Issue
Block a user