import sys, copy, os, re
import yaml
IN = sys.argv[1] if len(sys.argv) > 1 else 'target/specs/l1_model_fixed.yaml'
OUT = sys.argv[2] if len(sys.argv) > 2 else 'target/specs/l2_rust_compatible.yaml'
REPORT = 'target/reports/l2_rust_compat_report.md'
with open(IN, 'r', encoding='utf-8') as f:
doc = yaml.safe_load(f)
changes = []
def note(msg):
changes.append(msg)
def walk_and_prune(node):
if isinstance(node, dict):
if 'default' in node: node.pop('default', None); note('Removed default at some nodes')
node.pop('optional', None)
node.pop('$recursiveRef', None)
node.pop('$recursiveAnchor', None)
node.pop('propertyNames', None)
for v in node.values():
walk_and_prune(v)
elif isinstance(node, list):
for v in node:
walk_and_prune(v)
def replace_refs(node, from_ref, to_ref):
if isinstance(node, dict):
if node.get('$ref') == from_ref:
node['$ref'] = to_ref
for v in node.values():
replace_refs(v, from_ref, to_ref)
elif isinstance(node, list):
for v in node:
replace_refs(v, from_ref, to_ref)
def strip_ref_siblings(node):
if isinstance(node, dict):
if '$ref' in node and len(node.keys()) > 1:
ref = node['$ref']
node.clear()
node['$ref'] = ref
note('Stripped $ref siblings on a schema node')
for v in node.values():
strip_ref_siblings(v)
elif isinstance(node, list):
for v in node:
strip_ref_siblings(v)
schemas = doc.get('components', {}).get('schemas', {})
walk_and_prune(schemas)
strip_ref_siblings(doc)
def set_prop(schema_name, prop, new_schema):
s = schemas.get(schema_name)
if s and isinstance(s, dict):
props = s.setdefault('properties', {})
if prop in props:
props[prop] = new_schema
note(f"Set {schema_name}.{prop} -> {new_schema}")
set_prop('CreateEmbeddingRequest', 'model', {'type':'string'})
set_prop('CreateCompletionRequest', 'prompt', {'type':'string'})
set_prop('CreateModerationRequest', 'input', {'type':'string'})
set_prop('RealtimeResponseCreateParams', 'conversation', {'type':'string'})
set_prop('CreateImageEditRequest', 'model', {'type':'string'})
set_prop('CreateImageEditRequest', 'image', {'type':'string'})
set_prop('CreateImageVariationRequest', 'model', {'type':'string'})
set_prop('CreateImageRequest', 'model', {'type':'string'})
set_prop('CreateTranscriptionRequest', 'model', {'type':'string'})
set_prop('CreateTranslationRequest', 'model', {'type':'string'})
set_prop('CreateChatCompletionRequest', 'model', {'type':'string'})
set_prop('CreateModerationRequest', 'model', {'type':'string'})
set_prop('CreateResponse', 'model', {'type':'string'})
for (schema, prop) in [
('CreateMessageRequest','content'),
('PredictionContent','content'),
]:
set_prop(schema, prop, {'type':'string'})
cr = schemas.get('CreateResponse')
if isinstance(cr, dict) and isinstance(cr.get('allOf'), list):
for comp in cr['allOf']:
if isinstance(comp, dict) and comp.get('type') == 'object':
props = comp.setdefault('properties', {})
if 'input' in props:
props['input'] = {'type': 'string'}
if 'model' in props:
props['model'] = {'type': 'string'}
set_prop('CreateTranslationRequest', 'model', {'type':'string'})
def normalize_chat_content_titles(schema_name: str):
s = schemas.get(schema_name)
if not isinstance(s, dict):
return
props = s.get('properties', {})
c = props.get('content')
if isinstance(c, dict) and isinstance(c.get('oneOf'), list) and len(c['oneOf']) == 2:
if isinstance(c['oneOf'][0], dict) and c['oneOf'][0].get('type') == 'string':
c['oneOf'][0]['title'] = 'Text'
if isinstance(c['oneOf'][1], dict) and c['oneOf'][1].get('type') == 'array':
c['oneOf'][1]['title'] = 'ArrayOfContentParts'
note(f"Normalized {schema_name}.content.oneOf titles -> Text / ArrayOfContentParts")
for name in [
'ChatCompletionRequestSystemMessage',
'ChatCompletionRequestUserMessage',
'ChatCompletionRequestAssistantMessage',
'ChatCompletionRequestToolMessage',
'ChatCompletionRequestDeveloperMessage',
]:
normalize_chat_content_titles(name)
cer = schemas.get('CreateEmbeddingRequest')
if isinstance(cer, dict):
props = cer.get('properties', {})
inp = props.get('input')
if isinstance(inp, dict) and isinstance(inp.get('oneOf'), list):
for item in inp['oneOf']:
if not isinstance(item, dict):
continue
t = item.get('type')
if t == 'string':
item['title'] = 'Text'
elif t == 'array':
it = item.get('items')
if isinstance(it, dict) and it.get('type') == 'string':
item['title'] = 'ArrayOfStrings'
elif isinstance(it, dict) and it.get('type') == 'integer':
item['title'] = 'ArrayOfIntegers'
elif isinstance(it, dict) and it.get('type') == 'array':
inner = it.get('items')
if isinstance(inner, dict) and inner.get('type') == 'integer':
item['title'] = 'ArrayOfIntegerArrays'
note('Normalized CreateEmbeddingRequest.input.oneOf titles')
for key in ['CreateTranscriptionResponseStreamEvent','OutputItem','RealtimeClientEvent','RealtimeServerEvent','ResponseStreamEvent']:
if key in schemas:
schemas[key] = {'type':'object'}
note(f"Coerced schema {key} -> object")
for key in ['CreateTranscriptionRequestModel','CreateTranslationRequestModel','CreateImageVariationRequestModel','CreateEmbeddingRequestModel','CreateImageRequestModel']:
if key in schemas:
schemas[key] = {'type':'string'}
for key in ['CreateImageEditRequestModel','CreateImageEditRequestImage']:
if key in schemas:
schemas[key] = {'type':'string'}
if 'Type' in schemas:
schemas['TypeAction'] = schemas.pop('Type')
replace_refs(doc, '#/components/schemas/Type', '#/components/schemas/TypeAction')
note('Renamed schema Type -> TypeAction and updated refs')
hyphenated_schemas = [name for name in schemas.keys() if '-' in name]
for old_name in hyphenated_schemas:
new_name = re.sub(r'-(\d+)', r'\1', old_name) new_name = new_name.replace('-', '_')
if new_name != old_name:
schemas[new_name] = schemas.pop(old_name)
old_ref = f'#/components/schemas/{old_name}'
new_ref = f'#/components/schemas/{new_name}'
replace_refs(doc, old_ref, new_ref)
note(f'Normalized schema name: {old_name} -> {new_name}')
if 'ComputerAction' in schemas:
schemas['ComputerAction'] = {'type':'object'}
paths = doc.get('paths', {})
for path, item in list(paths.items()):
def ensure_path_param(name):
params = item.setdefault('parameters', [])
if not any(isinstance(p, dict) and p.get('in')=='path' and p.get('name')==name for p in params):
params.append({'name': name, 'in': 'path', 'required': True, 'schema': {'type':'string'}})
if '{certificate_id}' in path:
ensure_path_param('certificate_id')
if '{project_id}' in path:
ensure_path_param('project_id')
def fix_params_container(container, *, for_path: str):
if not isinstance(container, list):
return
for p in container:
if not isinstance(p, dict):
continue
if p.get('in') == 'query':
sch = p.get('schema')
name = p.get('name')
if for_path == '/fine_tuning/jobs' and name == 'metadata':
p['schema'] = {'type': 'object', 'additionalProperties': {'type': 'string'}}
p['style'] = 'deepObject'
p['explode'] = True
note('Restored deepObject for fine_tuning/jobs?metadata')
continue
if for_path == '/organization/audit_logs' and name == 'effective_at':
p['schema'] = {
'type': 'object',
'properties': {
'gt': {'type': 'integer'},
'gte': {'type': 'integer'},
'lt': {'type': 'integer'},
'lte': {'type': 'integer'},
}
}
p['style'] = 'deepObject'
p['explode'] = True
note('Restored deepObject for organization/audit_logs?effective_at')
continue
if (isinstance(sch, dict) and (sch.get('type') == 'object' or sch.get('$ref') == '#/components/schemas/Metadata')):
p['schema'] = {'type':'string'}
note(f"Coerced query param {p.get('name')} at {path} -> string")
p.pop('style', None)
p.pop('explode', None)
fix_params_container(item.get('parameters'), for_path=path)
for method in ['get','post','put','patch','delete','options','head']:
op = item.get(method)
if isinstance(op, dict):
fix_params_container(op.get('parameters'), for_path=path)
if '{certificate_id}' in path and isinstance(op.get('parameters'), list):
for p in op['parameters']:
if isinstance(p, dict) and p.get('in') == 'path' and p.get('name') == 'cert_id':
p['name'] = 'certificate_id'
note(f"Renamed op path param cert_id -> certificate_id at {path}")
with open(OUT, 'w', encoding='utf-8') as f:
yaml.safe_dump(doc, f, sort_keys=False)
print(f"Patched spec written to {OUT}")
with open(REPORT, 'w', encoding='utf-8') as rf:
rf.write('# Patch Report\n\n')
rf.write(f'- Source: {os.path.relpath(IN)}\n')
rf.write(f'- Output: {os.path.relpath(OUT)}\n')
rf.write('\n## Changes\n')
if not changes:
rf.write('- No structural changes recorded.\n')
else:
seen = set()
for c in changes:
if c not in seen:
rf.write(f'- {c}\n')
seen.add(c)