1
- import { marked } from 'marked'
1
+ import { instrument } from '@pydantic/logfire-cf-workers'
2
+ import { marked } from 'marked'
2
3
3
- export default {
4
+ const handler = {
4
5
async fetch ( request , env ) : Promise < Response > {
5
6
const url = new URL ( request . url )
6
7
if ( url . pathname === '/changelog.html' ) {
7
8
const changelog = await getChangelog ( env . KV , env . GIT_COMMIT_SHA )
8
9
return new Response ( changelog , { headers : { 'content-type' : 'text/html' } } )
9
10
}
11
+ const maybeTextResponse = await maybeGetTextResponse ( request , env )
12
+ if ( maybeTextResponse ) {
13
+ return maybeTextResponse
14
+ }
10
15
const r = await env . ASSETS . fetch ( request )
11
- if ( r . status == 404 ) {
16
+ if ( r . status === 404 ) {
12
17
const redirectPath = redirect ( url . pathname )
13
18
if ( redirectPath ) {
14
- url . pathname = redirectPath
15
- return Response . redirect ( url . toString ( ) , 301 )
19
+ if ( redirectPath . startsWith ( 'http' ) ) {
20
+ return Response . redirect ( redirectPath , 301 )
21
+ } else {
22
+ url . pathname = redirectPath
23
+ return Response . redirect ( url . toString ( ) , 301 )
24
+ }
16
25
}
17
26
url . pathname = '/404.html'
18
27
const r = await env . ASSETS . fetch ( url )
@@ -22,14 +31,34 @@ export default {
22
31
} ,
23
32
} satisfies ExportedHandler < Env >
24
33
34
+ export default instrument ( handler , {
35
+ service : {
36
+ name : 'pai-docs' ,
37
+ } ,
38
+ baseUrl : 'https://api.logfire.dev' ,
39
+ } )
40
+
25
41
const redirect_lookup : Record < string , string > = {
26
42
'/common_tools' : '/common-tools/' ,
27
43
'/testing-evals' : '/testing/' ,
28
44
'/result' : '/output/' ,
45
+ '/mcp/run-python' : 'https://github.com/pydantic/mcp-run-python' ,
46
+ '/temporal' : '/durable_execution/temporal/' ,
47
+ '/api' : '/api/agent/' ,
48
+ '/examples/question-graph' : '/graph/' ,
49
+ '/api/models/vertexai' : '/models/google/' ,
50
+ '/models/gemini' : '/models/google/' ,
51
+ '/api/models/gemini' : '/api/models/google/' ,
52
+ '/contributing' : '/contributing/' ,
53
+ '/api/format_as_xml' : '/api/format_prompt/' ,
54
+ '/api/models/ollama' : '/models/openai/#ollama' ,
55
+ '/examples' : 'examples/setup/' ,
56
+ '/mcp' : '/mcp/overview/' ,
57
+ '/models' : '/models/overview/' ,
29
58
}
30
59
31
60
function redirect ( pathname : string ) : string | null {
32
- return redirect_lookup [ pathname . replace ( / \/ + $ / , '' ) ] ?? null
61
+ return redirect_lookup [ pathname . replace ( / [ / : ] + $ / , '' ) ] ?? null
33
62
}
34
63
35
64
async function getChangelog ( kv : KVNamespace , commitSha : string ) : Promise < string > {
@@ -44,8 +73,8 @@ async function getChangelog(kv: KVNamespace, commitSha: string): Promise<string>
44
73
}
45
74
let url : string | undefined = 'https://api.github.com/repos/pydantic/pydantic-ai/releases'
46
75
const releases : Release [ ] = [ ]
47
- while ( typeof url == 'string' ) {
48
- const response = await fetch ( url , { headers } )
76
+ while ( typeof url === 'string' ) {
77
+ const response : Response = await fetch ( url , { headers } )
49
78
if ( ! response . ok ) {
50
79
const text = await response . text ( )
51
80
throw new Error ( `Failed to fetch changelog: ${ response . status } ${ response . statusText } ${ text } ` )
@@ -77,7 +106,7 @@ function prepRelease(release: Release): string {
77
106
const body = release . body
78
107
. replace ( / ( # + ) / g, ( m ) => `##${ m } ` )
79
108
. replace ( / h t t p s : \/ \/ g i t h u b .c o m \/ p y d a n t i c \/ p y d a n t i c - a i \/ p u l l \/ ( \d + ) / g, ( url , id ) => `[#${ id } ](${ url } )` )
80
- . replace ( / ( \s ) @ ( [ \w \ -] + ) / g, ( _ , s , u ) => `${ s } [@${ u } ](https://github.com/${ u } )` )
109
+ . replace ( / ( \s ) @ ( [ \w - ] + ) / g, ( _ , s , u ) => `${ s } [@${ u } ](https://github.com/${ u } )` )
81
110
. replace ( / \* \* F u l l C h a n g e l o g \* \* : ( \S + ) / , ( _ , url ) => `[${ githubIcon } Compare diff](${ url } ).` )
82
111
return `
83
112
### ${ release . name }
@@ -87,3 +116,38 @@ ${body}
87
116
[${ githubIcon } View ${ release . tag_name } release](${ release . html_url } ).
88
117
`
89
118
}
119
+
120
+ /** Logic to return text (the markdown document where available) when the Accept header prefers plain text over html
121
+ * See https://x.com/threepointone/status/1971988718052651300
122
+ */
123
+ async function maybeGetTextResponse ( request : Request , env : Env ) : Promise < Response | undefined > {
124
+ if ( ! preferText ( request ) ) {
125
+ return
126
+ }
127
+ const url = new URL ( request . url )
128
+ url . pathname = `${ url . pathname . replace ( / [ / : ] + $ / , '' ) } /index.md`
129
+ const r = await env . ASSETS . fetch ( url )
130
+ if ( r . status === 200 ) {
131
+ return new Response ( r . body , {
132
+ headers : {
133
+ 'content-type' : 'text/plain' ,
134
+ } ,
135
+ } )
136
+ }
137
+ }
138
+
139
+ function preferText ( request : Request ) : boolean {
140
+ const accept = request . headers . get ( 'accept' )
141
+ if ( ! accept || request . method !== 'GET' ) {
142
+ return false
143
+ }
144
+ for ( const option of accept . split ( ',' ) ) {
145
+ const lowerOption = option . toLowerCase ( )
146
+ if ( lowerOption . includes ( 'html' ) ) {
147
+ return false
148
+ } else if ( lowerOption . includes ( 'text/plain' ) ) {
149
+ return true
150
+ }
151
+ }
152
+ return false
153
+ }
0 commit comments