Skip to content

Commit 5a4f61a

Browse files
markphelpsCopilotanish-sahoo
authored
Add x-cog-accept MIME type annotation for Path/File input fields (#2863)
* Add x-cog-accept MIME type annotation for Path/File input fields Add an 'accept' parameter to Input() that specifies allowed MIME types or file extensions for Path/File inputs. The Go static schema generator extracts this and emits it as 'x-cog-accept' in the OpenAPI schema, giving schema consumers (UIs, validators, API clients) visibility into what file types an input expects. Usage: Input(accept="image/*"), Input(accept="audio/wav,audio/mp3"), or Input(accept=".safetensors,.bin"). Using accept on non-Path/File types is a hard build error. * Set COG_STATIC_SCHEMA=1 in accept integration tests --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Anish Sahoo <anishsahoo2005@gmail.com>
1 parent 966752e commit 5a4f61a

9 files changed

Lines changed: 280 additions & 4 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Test that the accept parameter on Path/File inputs produces the
2+
# x-cog-accept annotation in the generated OpenAPI schema.
3+
#
4+
# Verifies:
5+
# - accept="image/*" on a Path input emits x-cog-accept in the schema
6+
# - accept with multiple MIME types works
7+
# - accept with file extensions works
8+
# - Fields without accept do not have x-cog-accept
9+
# - Prediction still works end-to-end
10+
11+
env COG_STATIC_SCHEMA=1
12+
13+
cog build -t $TEST_IMAGE
14+
15+
# Extract the schema from the image label
16+
exec docker inspect $TEST_IMAGE --format '{{index .Config.Labels "run.cog.openapi_schema"}}'
17+
18+
# x-cog-accept annotations are present
19+
stdout '"x-cog-accept":"image/\*"'
20+
stdout '"x-cog-accept":"audio/wav,audio/mp3"'
21+
stdout '"x-cog-accept":".safetensors,.bin"'
22+
23+
# The prompt field (str) should NOT have x-cog-accept
24+
# (we check the schema has prompt but confirm no extra x-cog-accept entries)
25+
stdout '"prompt":'
26+
27+
# Path fields still have uri format
28+
stdout '"format":"uri"'
29+
30+
# Prediction works end-to-end
31+
cog predict $TEST_IMAGE -i prompt=hello -i image=@test.png
32+
stdout 'hello-png'
33+
34+
-- cog.yaml --
35+
build:
36+
python_version: "3.12"
37+
predict: "predict.py:Predictor"
38+
39+
-- predict.py --
40+
from typing import Optional
41+
42+
from cog import BasePredictor, Input, Path
43+
44+
45+
class Predictor(BasePredictor):
46+
def predict(
47+
self,
48+
prompt: str = Input(description="Text prompt", default="test"),
49+
image: Path = Input(description="Input image", accept="image/*"),
50+
audio: Optional[Path] = Input(description="Audio clip", accept="audio/wav,audio/mp3", default=None),
51+
weights: Optional[Path] = Input(description="Model weights", accept=".safetensors,.bin", default=None),
52+
) -> str:
53+
ext = str(image).split(".")[-1]
54+
return f"{prompt}-{ext}"
55+
56+
-- test.png --
57+
fake image content
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Test that using accept on a non-Path/File input type causes a build error.
2+
#
3+
# The accept parameter is only valid on Path or File inputs. Using it on
4+
# str, int, float, etc. should produce a clear error at build time.
5+
6+
env COG_STATIC_SCHEMA=1
7+
8+
! cog build -t $TEST_IMAGE
9+
stderr 'accept is only valid on Path or File inputs'
10+
11+
-- cog.yaml --
12+
build:
13+
python_version: "3.12"
14+
predict: "predict.py:Predictor"
15+
16+
-- predict.py --
17+
from cog import BasePredictor, Input
18+
19+
20+
class Predictor(BasePredictor):
21+
def predict(
22+
self,
23+
name: str = Input(description="User name", accept="text/plain"),
24+
) -> str:
25+
return f"hello {name}"

pkg/schema/errors.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const (
2929
ErrChoicesNotResolvable
3030
ErrDefaultNotResolvable
3131
ErrUnresolvableType
32+
ErrAcceptOnNonFileType
3233
ErrOther
3334
)
3435

pkg/schema/openapi.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,11 @@ func buildInputSchema(info *PredictorInfo) (map[string]any, []enumSchema) {
438438
prop["deprecated"] = true
439439
}
440440

441+
// MIME type constraint for Path/File inputs
442+
if field.Accept != nil {
443+
prop["x-cog-accept"] = *field.Accept
444+
}
445+
441446
properties.Set(name, prop)
442447
})
443448

pkg/schema/openapi_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,6 +1007,80 @@ func TestMultipleInputTypes(t *testing.T) {
10071007
assert.NotContains(t, required, "secret_key")
10081008
}
10091009

1010+
// ---------------------------------------------------------------------------
1011+
// Tests: Accept (MIME type) annotation
1012+
// ---------------------------------------------------------------------------
1013+
1014+
func TestAcceptAnnotation(t *testing.T) {
1015+
accept := "image/*"
1016+
inputs := NewOrderedMap[string, InputField]()
1017+
inputs.Set("image", InputField{
1018+
Name: "image",
1019+
Order: 0,
1020+
FieldType: FieldType{Primitive: TypePath, Repetition: Required},
1021+
Accept: &accept,
1022+
})
1023+
1024+
info := &PredictorInfo{
1025+
Inputs: inputs,
1026+
Output: SchemaPrim(TypeString),
1027+
Mode: ModePredict,
1028+
}
1029+
1030+
spec := parseSpec(t, info)
1031+
props := getPath(spec, "components", "schemas", "Input", "properties").(map[string]any)
1032+
1033+
imageField := props["image"].(map[string]any)
1034+
assert.Equal(t, "string", imageField["type"])
1035+
assert.Equal(t, "uri", imageField["format"])
1036+
assert.Equal(t, "image/*", imageField["x-cog-accept"])
1037+
}
1038+
1039+
func TestAcceptAnnotationMultipleMimeTypes(t *testing.T) {
1040+
accept := "audio/wav,audio/mp3,audio/flac"
1041+
inputs := NewOrderedMap[string, InputField]()
1042+
inputs.Set("audio", InputField{
1043+
Name: "audio",
1044+
Order: 0,
1045+
FieldType: FieldType{Primitive: TypePath, Repetition: Required},
1046+
Accept: &accept,
1047+
})
1048+
1049+
info := &PredictorInfo{
1050+
Inputs: inputs,
1051+
Output: SchemaPrim(TypeString),
1052+
Mode: ModePredict,
1053+
}
1054+
1055+
spec := parseSpec(t, info)
1056+
props := getPath(spec, "components", "schemas", "Input", "properties").(map[string]any)
1057+
1058+
audioField := props["audio"].(map[string]any)
1059+
assert.Equal(t, "audio/wav,audio/mp3,audio/flac", audioField["x-cog-accept"])
1060+
}
1061+
1062+
func TestAcceptAnnotationNotPresentWhenNil(t *testing.T) {
1063+
inputs := NewOrderedMap[string, InputField]()
1064+
inputs.Set("image", InputField{
1065+
Name: "image",
1066+
Order: 0,
1067+
FieldType: FieldType{Primitive: TypePath, Repetition: Required},
1068+
})
1069+
1070+
info := &PredictorInfo{
1071+
Inputs: inputs,
1072+
Output: SchemaPrim(TypeString),
1073+
Mode: ModePredict,
1074+
}
1075+
1076+
spec := parseSpec(t, info)
1077+
props := getPath(spec, "components", "schemas", "Input", "properties").(map[string]any)
1078+
1079+
imageField := props["image"].(map[string]any)
1080+
_, hasAccept := imageField["x-cog-accept"]
1081+
assert.False(t, hasAccept, "x-cog-accept should not be present when Accept is nil")
1082+
}
1083+
10101084
// ---------------------------------------------------------------------------
10111085
// Tests: Edge cases
10121086
// ---------------------------------------------------------------------------

pkg/schema/python/inputs.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type inputCallInfo struct {
1818
Regex *string
1919
Choices []schema.DefaultValue
2020
Deprecated *bool
21+
Accept *string
2122
}
2223

2324
type inputMethodInfo struct {
@@ -271,7 +272,11 @@ func inputField(name string, order int, inputType schema.InputType, fieldType sc
271272
}
272273
}
273274

274-
func inputFieldWithInfo(name string, order int, inputType schema.InputType, fieldType schema.FieldType, info inputCallInfo) schema.InputField {
275+
func inputFieldWithInfo(name string, order int, inputType schema.InputType, fieldType schema.FieldType, info inputCallInfo) (schema.InputField, error) {
276+
if info.Accept != nil && fieldType.Primitive != schema.TypePath && fieldType.Primitive != schema.TypeFile {
277+
return schema.InputField{}, schema.NewError(schema.ErrAcceptOnNonFileType,
278+
fmt.Sprintf("accept is only valid on Path or File inputs (parameter '%s')", name))
279+
}
275280
field := inputField(name, order, inputType, fieldType)
276281
field.Default = info.Default
277282
field.Description = info.Description
@@ -282,7 +287,8 @@ func inputFieldWithInfo(name string, order int, inputType schema.InputType, fiel
282287
field.Regex = info.Regex
283288
field.Choices = info.Choices
284289
field.Deprecated = info.Deprecated
285-
return field
290+
field.Accept = info.Accept
291+
return field, nil
286292
}
287293

288294
func firstParamIsSelf(params *sitter.Node, source []byte) bool {
@@ -398,13 +404,19 @@ func parseTypedDefaultParameter(node *sitter.Node, source []byte, order int, ctx
398404
if err != nil {
399405
return schema.InputField{}, err
400406
}
401-
field := inputFieldWithInfo(name, order, inputType, fieldType, info)
407+
field, err := inputFieldWithInfo(name, order, inputType, fieldType, info)
408+
if err != nil {
409+
return schema.InputField{}, err
410+
}
402411
return field, schema.ValidateInputField(field)
403412
}
404413

405414
// 2. Reference to Input() via class attribute or static method
406415
if info, ok := resolveInputReference(valNode, source, ctx.registry); ok {
407-
field := inputFieldWithInfo(name, order, inputType, fieldType, info)
416+
field, err := inputFieldWithInfo(name, order, inputType, fieldType, info)
417+
if err != nil {
418+
return schema.InputField{}, err
419+
}
408420
return field, schema.ValidateInputField(field)
409421
}
410422

@@ -511,6 +523,10 @@ func parseInputCall(node *sitter.Node, source []byte, paramName string, scope mo
511523
if b, ok := parseBoolLiteral(valNode, source); ok {
512524
info.Deprecated = &b
513525
}
526+
case "accept":
527+
if s, ok := parseStringLiteral(valNode, source); ok {
528+
info.Accept = &s
529+
}
514530
}
515531
}
516532

pkg/schema/python/parser_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1653,6 +1653,97 @@ class Predictor(BasePredictor):
16531653
require.True(t, *old.Deprecated)
16541654
}
16551655

1656+
// ---------------------------------------------------------------------------
1657+
// Accept MIME type
1658+
// ---------------------------------------------------------------------------
1659+
1660+
func TestAcceptMimeType(t *testing.T) {
1661+
source := `
1662+
from cog import BasePredictor, Input, Path
1663+
1664+
class Predictor(BasePredictor):
1665+
def predict(self, image: Path = Input(description="An image", accept="image/*")) -> str:
1666+
pass
1667+
`
1668+
info := parse(t, source, "Predictor")
1669+
image, ok := info.Inputs.Get("image")
1670+
require.True(t, ok)
1671+
require.NotNil(t, image.Accept)
1672+
require.Equal(t, "image/*", *image.Accept)
1673+
}
1674+
1675+
func TestAcceptMultipleMimeTypes(t *testing.T) {
1676+
source := `
1677+
from cog import BasePredictor, Input, Path
1678+
1679+
class Predictor(BasePredictor):
1680+
def predict(self, audio: Path = Input(accept="audio/wav,audio/mp3")) -> str:
1681+
pass
1682+
`
1683+
info := parse(t, source, "Predictor")
1684+
audio, ok := info.Inputs.Get("audio")
1685+
require.True(t, ok)
1686+
require.NotNil(t, audio.Accept)
1687+
require.Equal(t, "audio/wav,audio/mp3", *audio.Accept)
1688+
}
1689+
1690+
func TestAcceptFileExtensions(t *testing.T) {
1691+
source := `
1692+
from cog import BasePredictor, Input, Path
1693+
1694+
class Predictor(BasePredictor):
1695+
def predict(self, weights: Path = Input(accept=".safetensors,.bin")) -> str:
1696+
pass
1697+
`
1698+
info := parse(t, source, "Predictor")
1699+
weights, ok := info.Inputs.Get("weights")
1700+
require.True(t, ok)
1701+
require.NotNil(t, weights.Accept)
1702+
require.Equal(t, ".safetensors,.bin", *weights.Accept)
1703+
}
1704+
1705+
func TestAcceptOnFileType(t *testing.T) {
1706+
source := `
1707+
from cog import BasePredictor, Input, File
1708+
1709+
class Predictor(BasePredictor):
1710+
def predict(self, f: File = Input(accept="image/png")) -> str:
1711+
pass
1712+
`
1713+
info := parse(t, source, "Predictor")
1714+
f, ok := info.Inputs.Get("f")
1715+
require.True(t, ok)
1716+
require.NotNil(t, f.Accept)
1717+
require.Equal(t, "image/png", *f.Accept)
1718+
}
1719+
1720+
func TestAcceptOnNonFileTypeErrors(t *testing.T) {
1721+
source := `
1722+
from cog import BasePredictor, Input
1723+
1724+
class Predictor(BasePredictor):
1725+
def predict(self, name: str = Input(accept="image/*")) -> str:
1726+
pass
1727+
`
1728+
se := parseErr(t, source, "Predictor", schema.ModePredict)
1729+
require.Equal(t, schema.ErrAcceptOnNonFileType, se.Kind)
1730+
require.Contains(t, se.Error(), "name")
1731+
}
1732+
1733+
func TestAcceptNotSetWhenOmitted(t *testing.T) {
1734+
source := `
1735+
from cog import BasePredictor, Input, Path
1736+
1737+
class Predictor(BasePredictor):
1738+
def predict(self, image: Path = Input(description="An image")) -> str:
1739+
pass
1740+
`
1741+
info := parse(t, source, "Predictor")
1742+
image, ok := info.Inputs.Get("image")
1743+
require.True(t, ok)
1744+
require.Nil(t, image.Accept)
1745+
}
1746+
16561747
// ---------------------------------------------------------------------------
16571748
// File type (deprecated alias for Path)
16581749
// ---------------------------------------------------------------------------

pkg/schema/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ type InputField struct {
231231
Regex *string
232232
Choices []DefaultValue
233233
Deprecated *bool
234+
Accept *string // MIME types / file extensions for Path/File inputs (e.g. "image/*")
234235
}
235236

236237
// IsRequired returns true if this field is required in the schema.

python/cog/input.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class FieldInfo:
2727
regex: Optional[str] = None
2828
choices: Optional[List[Union[str, int]]] = None
2929
deprecated: Optional[bool] = None
30+
accept: Optional[str] = None
3031

3132

3233
def Input(
@@ -41,6 +42,7 @@ def Input(
4142
regex: Optional[str] = None,
4243
choices: Optional[List[Union[str, int]]] = None,
4344
deprecated: Optional[bool] = None,
45+
accept: Optional[str] = None,
4446
) -> Any:
4547
"""
4648
Create an input field specification for a predictor parameter.
@@ -70,6 +72,9 @@ def predict(
7072
regex: Regular expression pattern for string inputs.
7173
choices: List of allowed values.
7274
deprecated: Whether the input is deprecated.
75+
accept: Allowed MIME types or file extensions for Path/File inputs,
76+
using the same format as the HTML accept attribute (e.g.
77+
``"image/*"``, ``"image/png,image/jpeg"``, ``".safetensors,.bin"``).
7378
7479
Returns:
7580
A FieldInfo instance containing the field metadata.
@@ -92,4 +97,5 @@ def predict(
9297
regex=regex,
9398
choices=choices,
9499
deprecated=deprecated,
100+
accept=accept,
95101
)

0 commit comments

Comments
 (0)