1- import { marked } from 'marked'
1+ import { instrument } from '@pydantic/logfire-cf-workers'
2+ import { marked } from 'marked'
23
3- export default {
4+ const handler = {
45 async fetch ( request , env ) : Promise < Response > {
56 const url = new URL ( request . url )
67 if ( url . pathname === '/changelog.html' ) {
78 const changelog = await getChangelog ( env . KV , env . GIT_COMMIT_SHA )
89 return new Response ( changelog , { headers : { 'content-type' : 'text/html' } } )
910 }
11+ const maybeTextResponse = await maybeGetTextResponse ( request , env )
12+ if ( maybeTextResponse ) {
13+ return maybeTextResponse
14+ }
1015 const r = await env . ASSETS . fetch ( request )
11- if ( r . status == 404 ) {
16+ if ( r . status === 404 ) {
1217 const redirectPath = redirect ( url . pathname )
1318 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+ }
1625 }
1726 url . pathname = '/404.html'
1827 const r = await env . ASSETS . fetch ( url )
@@ -22,14 +31,34 @@ export default {
2231 } ,
2332} satisfies ExportedHandler < Env >
2433
34+ export default instrument ( handler , {
35+ service : {
36+ name : 'pai-docs' ,
37+ } ,
38+ baseUrl : 'https://api.logfire.dev' ,
39+ } )
40+
2541const redirect_lookup : Record < string , string > = {
2642 '/common_tools' : '/common-tools/' ,
2743 '/testing-evals' : '/testing/' ,
2844 '/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/' ,
2958}
3059
3160function redirect ( pathname : string ) : string | null {
32- return redirect_lookup [ pathname . replace ( / \/ + $ / , '' ) ] ?? null
61+ return redirect_lookup [ pathname . replace ( / [ / : ] + $ / , '' ) ] ?? null
3362}
3463
3564async function getChangelog ( kv : KVNamespace , commitSha : string ) : Promise < string > {
@@ -44,8 +73,8 @@ async function getChangelog(kv: KVNamespace, commitSha: string): Promise<string>
4473 }
4574 let url : string | undefined = 'https://api.github.com/repos/pydantic/pydantic-ai/releases'
4675 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 } )
4978 if ( ! response . ok ) {
5079 const text = await response . text ( )
5180 throw new Error ( `Failed to fetch changelog: ${ response . status } ${ response . statusText } ${ text } ` )
@@ -77,7 +106,7 @@ function prepRelease(release: Release): string {
77106 const body = release . body
78107 . replace ( / ( # + ) / g, ( m ) => `##${ m } ` )
79108 . 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 } )` )
81110 . replace ( / \* \* F u l l C h a n g e l o g \* \* : ( \S + ) / , ( _ , url ) => `[${ githubIcon } Compare diff](${ url } ).` )
82111 return `
83112### ${ release . name }
@@ -87,3 +116,38 @@ ${body}
87116[${ githubIcon } View ${ release . tag_name } release](${ release . html_url } ).
88117`
89118}
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