Skip to content

Commit 63cf1d8

Browse files
tempusfrangitmarkphelpsmichaeldwan
authored
feat: add static schema generation with legacy fallback (0.17 gated) (#2788)
* fix: support dict and bare list as prediction output types in schema gen The tree-sitter schema parser rejected unparameterized dict and list return types with 'unsupported type'. This broke the resnet example and any predictor returning -> dict or -> list. - dict/Dict map to TypeAny (JSON Schema: {"type": "object"}) - list/List map to OutputList with TypeAny (JSON Schema: {"type": "array", "items": {"type": "object"}}) This matches the old Python schema gen behavior exactly: dict -> Dict[str, Any] -> {"type": "object"} list -> List[Any] -> {"type": "array", "items": {"type": "object"}} * feat: recursive SchemaType ADT, cross-file model resolution, comprehensive tests Replace the flat OutputType system with a recursive SchemaType algebraic data type that supports arbitrary nesting (dict[str, list[dict[str, int]]], etc.). Key changes: - SchemaType ADT with 7 kinds: Primitive, Any, Array, Dict, Object, Iterator, ConcatIterator - ResolveSchemaType: recursive resolver replacing ResolveOutputType - Cross-file model resolution: imports from local .py files are found on disk, parsed with tree-sitter, and BaseModel subclasses extracted automatically - Handles all local import permutations: relative, dotted, subpackage, aliased - Clear error messages for unresolvable types (includes import source and guidance) - Remove legacy OutputType, OutputKind, ObjectField, ResolveOutputType - Thread sourceDir through Parser -> ParsePredictor for filesystem access - Rewrite architecture/02-schema.md for the static Go parser Tests: 93 unit tests (12 recursive nesting, 5 unresolvable errors, 3 pydantic compat, 11 cross-file resolution, 1 end-to-end schema gen) + 1 integration test * fix: address review — propagate dict errors, DRY optional unwrap, recursive model fields - Propagate errors from dict value type resolution instead of silently falling back to opaque SchemaAny (dict[str, Tensor] now errors) - Extract UnwrapOptional helper used by ResolveFieldType, resolveUnionSchemaType, and resolveFieldSchemaType (3 callsites) - resolveModelToSchemaType now uses ResolveSchemaType via resolveFieldSchemaType, supporting dict/nested types inside BaseModel fields (previously limited to primitives, Optional[T], List[T]) - Fix stale comments: Optional rejected not nullable, Iterator allows nested types - Fix garbled unicode in architecture docs, fix SchemaAny table entry * fix: address review round 2 — nullable bug, dead fields, error handling - Fix nullable incorrectly set on non-required fields instead of field.Type.Nullable (debug: bool = False was appearing nullable) - Remove dead CogArrayType/CogArrayDisplay struct fields (hardcoded in coreSchema, never read from struct) - Distinguish os.ErrNotExist from permission errors in cross-file resolution; warn on parse failures instead of silently ignoring - Fix bare dict/list JSON Schema examples in architecture docs - Add regression tests: defaulted non-Optional field not nullable, Optional field nullable in JSON Schema, dict[str, Tensor] errors instead of silently producing SchemaAny (both top-level and inside BaseModel fields) * test: add Go native fuzz tests for schema generation Four fuzz targets exercising the schema pipeline: - FuzzResolveSchemaType: arbitrary TypeAnnotation trees through the recursive resolver - FuzzJSONSchema: random SchemaType trees through JSON Schema rendering - FuzzParsePredictor: arbitrary bytes as Python source through the tree-sitter parser - FuzzParseTypeAnnotation: arbitrary return type strings in predict signatures Includes: - mise task 'test:fuzz' (FUZZTIME=30s per target by default) - CI job 'fuzz-go' running 30s per target on Go changes - Byte encoder/decoder for deterministic TypeAnnotation and SchemaType tree construction from fuzz corpus * fix: combine appends to satisfy gocritic lint * feat: opt-in static schema gen with legacy runtime fallback (#2800) * fix: make static schema generation opt-in via COG_STATIC_SCHEMA env var The static Go tree-sitter schema generator was the default for SDK >= 0.17.0, which risks breaking builds when the parser encounters types it cannot resolve. - Gate static schema gen behind COG_STATIC_SCHEMA=1 (or "true") env var - Legacy runtime schema generation (boot container + python introspection) remains the default - When opted in, gracefully fall back to legacy on ErrUnresolvableType instead of hard-failing the build - Add unit tests for canUseStaticSchemaGen (12 table-driven cases) - Add integration test for the static->legacy fallback path - Update existing static/multi-file integration tests to set the env var * docs: update schema architecture doc for opt-in static generation model Reflect that static schema generation is opt-in via COG_STATIC_SCHEMA=1, with legacy runtime path as the default and automatic fallback. * fix: restore legacy runtime schema generation modules for fallback path Restore the Python modules needed by the legacy runtime schema generation path (python -m cog.command.openapi_schema). These were deleted in 61eedf3 but are needed as the fallback when the static Go parser encounters types it cannot resolve. Restored modules: _adt, _inspector, _schemas, coder, config, errors, mode, suppress_output, command/__init__, command/openapi_schema. config.py is trimmed to only what openapi_schema.py needs (removed get_predictor_types which depended on deleted get_predict/get_train). * fix: simplify schema gen gating — let coglet handle missing schema for train/serve Remove the skipLabels override that forced static schema gen for cog train/serve paths. Now useStatic is purely opt-in via COG_STATIC_SCHEMA=1 for all commands. For train/serve without the env var, no schema is generated at build time. Coglet gracefully handles this (warns and accepts all input). These are local-only images that don't need strict schema validation. Also improves the static_schema_fallback integration test to use a realistic mypackage/__init__.py scenario instead of a plain class. * fix: restore static gen for train/serve paths, handle pydantic v2 in legacy inspector Two IT failures: 1. training_setup: cog train needs schema for -i flag parsing. The CLI fetches it from coglet's /openapi.json, which returns 503 when no schema file exists. Re-enable static gen for skipLabels paths (same as main) since there's no post-build legacy fallback for these. 2. pydantic2_output: the legacy runtime inspector (_inspector.py) didn't handle pydantic v2 BaseModel as output types — only cog.BaseModel. Add conditional pydantic.BaseModel check with model_fields iteration. * fix: suppress pyright reportMissingImports for optional pydantic import * fix: flatten nested error handling to early returns, case-insensitive env var check * chore: remove debug logging from _schemas.py * Gate static schema generation behind COG_STATIC_SCHEMA=1 for all commands Previously, cog train/predict/serve (skipLabels=true) always used static schema generation, bypassing the COG_STATIC_SCHEMA env var check. Now all commands require COG_STATIC_SCHEMA=1 to enable static schema generation. * test: skip train integration tests that require static schema gen * fix: restore legacy schema gen for cog train/predict/serve (skipLabels path) The skipLabels optimization (8b1c141) skipped the entire post-build phase including legacy schema generation. This broke cog train/predict/serve which need the schema for -i flag parsing and input validation. Move legacy schema gen above the skipLabels early return and add a minimal second Docker build that bundles only the schema file (no labels, pip freeze, or git info). Restore the sourceDir parameter on GenerateOpenAPISchema so ExcludeSource builds can volume-mount the project directory for Python introspection. Re-enable the train_basic and training_setup integration tests that were temporarily skipped. * test: skip train integration tests that require static schema gen --------- Co-authored-by: Mark Phelps <209477+markphelps@users.noreply.github.com> Co-authored-by: Michael Dwan <mdwan@cloudflare.com> Co-authored-by: Mark Phelps <mphelps@cloudflare.com>
1 parent 8c44a7a commit 63cf1d8

33 files changed

Lines changed: 4191 additions & 393 deletions

.github/workflows/ci.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,28 @@ jobs:
542542
gotestsum -- -short -timeout 1200s -parallel 5 ./... &
543543
wait $!
544544
545+
fuzz-go:
546+
name: Fuzz Go
547+
needs: changes
548+
if: needs.changes.outputs.go == 'true'
549+
runs-on: ubuntu-latest
550+
timeout-minutes: 10
551+
env:
552+
CGO_ENABLED: "1"
553+
steps:
554+
- uses: actions/checkout@v6
555+
- uses: actions/setup-go@v6
556+
with:
557+
go-version-file: go.mod
558+
- name: Fuzz schema type resolution
559+
run: go test ./pkg/schema/ -run='^$' -fuzz=FuzzResolveSchemaType -fuzztime=30s
560+
- name: Fuzz JSON schema generation
561+
run: go test ./pkg/schema/ -run='^$' -fuzz=FuzzJSONSchema -fuzztime=30s
562+
- name: Fuzz Python parser
563+
run: go test ./pkg/schema/python/ -run='^$' -fuzz=FuzzParsePredictor -fuzztime=30s
564+
- name: Fuzz type annotation parsing
565+
run: go test ./pkg/schema/python/ -run='^$' -fuzz=FuzzParseTypeAnnotation -fuzztime=30s
566+
545567
test-rust:
546568
name: Test Rust
547569
needs: changes

0 commit comments

Comments
 (0)